1#!/usr/bin/env python3
2
3#----------------------------------------------------------------------
4# Be sure to add the python path that points to the LLDB shared library.
5#
6# To use this in the embedded python interpreter using "lldb":
7#
8#   cd /path/containing/crashlog.py
9#   lldb
10#   (lldb) script import crashlog
11#   "crashlog" command installed, type "crashlog --help" for detailed help
12#   (lldb) crashlog ~/Library/Logs/DiagnosticReports/a.crash
13#
14# The benefit of running the crashlog command inside lldb in the
15# embedded python interpreter is when the command completes, there
16# will be a target with all of the files loaded at the locations
17# described in the crash log. Only the files that have stack frames
18# in the backtrace will be loaded unless the "--load-all" option
19# has been specified. This allows users to explore the program in the
20# state it was in right at crash time.
21#
22# On MacOSX csh, tcsh:
23#   ( setenv PYTHONPATH /path/to/LLDB.framework/Resources/Python ; ./crashlog.py ~/Library/Logs/DiagnosticReports/a.crash )
24#
25# On MacOSX sh, bash:
26#   PYTHONPATH=/path/to/LLDB.framework/Resources/Python ./crashlog.py ~/Library/Logs/DiagnosticReports/a.crash
27#----------------------------------------------------------------------
28
29from __future__ import print_function
30import cmd
31import datetime
32import glob
33import optparse
34import os
35import platform
36import plistlib
37import re
38import shlex
39import string
40import subprocess
41import sys
42import time
43import uuid
44import json
45
46try:
47    # First try for LLDB in case PYTHONPATH is already correctly setup.
48    import lldb
49except ImportError:
50    # Ask the command line driver for the path to the lldb module. Copy over
51    # the environment so that SDKROOT is propagated to xcrun.
52    env = os.environ.copy()
53    env['LLDB_DEFAULT_PYTHON_VERSION'] = str(sys.version_info.major)
54    command =  ['xcrun', 'lldb', '-P'] if platform.system() == 'Darwin' else ['lldb', '-P']
55    # Extend the PYTHONPATH if the path exists and isn't already there.
56    lldb_python_path = subprocess.check_output(command, env=env).decode("utf-8").strip()
57    if os.path.exists(lldb_python_path) and not sys.path.__contains__(lldb_python_path):
58        sys.path.append(lldb_python_path)
59    # Try importing LLDB again.
60    try:
61        import lldb
62    except ImportError:
63        print("error: couldn't locate the 'lldb' module, please set PYTHONPATH correctly")
64        sys.exit(1)
65
66from lldb.utils import symbolication
67
68
69def read_plist(s):
70    if sys.version_info.major == 3:
71        return plistlib.loads(s)
72    else:
73        return plistlib.readPlistFromString(s)
74
75class CrashLog(symbolication.Symbolicator):
76    class Thread:
77        """Class that represents a thread in a darwin crash log"""
78
79        def __init__(self, index, app_specific_backtrace):
80            self.index = index
81            self.frames = list()
82            self.idents = list()
83            self.registers = dict()
84            self.reason = None
85            self.queue = None
86            self.crashed = False
87            self.app_specific_backtrace = app_specific_backtrace
88
89        def dump(self, prefix):
90            if self.app_specific_backtrace:
91                print("%Application Specific Backtrace[%u] %s" % (prefix, self.index, self.reason))
92            else:
93                print("%sThread[%u] %s" % (prefix, self.index, self.reason))
94            if self.frames:
95                print("%s  Frames:" % (prefix))
96                for frame in self.frames:
97                    frame.dump(prefix + '    ')
98            if self.registers:
99                print("%s  Registers:" % (prefix))
100                for reg in self.registers.keys():
101                    print("%s    %-8s = %#16.16x" % (prefix, reg, self.registers[reg]))
102
103        def dump_symbolicated(self, crash_log, options):
104            this_thread_crashed = self.app_specific_backtrace
105            if not this_thread_crashed:
106                this_thread_crashed = self.did_crash()
107                if options.crashed_only and this_thread_crashed == False:
108                    return
109
110            print("%s" % self)
111            display_frame_idx = -1
112            for frame_idx, frame in enumerate(self.frames):
113                disassemble = (
114                    this_thread_crashed or options.disassemble_all_threads) and frame_idx < options.disassemble_depth
115                if frame_idx == 0:
116                    symbolicated_frame_addresses = crash_log.symbolicate(
117                        frame.pc & crash_log.addr_mask, options.verbose)
118                else:
119                    # Any frame above frame zero and we have to subtract one to
120                    # get the previous line entry
121                    symbolicated_frame_addresses = crash_log.symbolicate(
122                        (frame.pc & crash_log.addr_mask) - 1, options.verbose)
123
124                if symbolicated_frame_addresses:
125                    symbolicated_frame_address_idx = 0
126                    for symbolicated_frame_address in symbolicated_frame_addresses:
127                        display_frame_idx += 1
128                        print('[%3u] %s' % (frame_idx, symbolicated_frame_address))
129                        if (options.source_all or self.did_crash(
130                        )) and display_frame_idx < options.source_frames and options.source_context:
131                            source_context = options.source_context
132                            line_entry = symbolicated_frame_address.get_symbol_context().line_entry
133                            if line_entry.IsValid():
134                                strm = lldb.SBStream()
135                                if line_entry:
136                                    crash_log.debugger.GetSourceManager().DisplaySourceLinesWithLineNumbers(
137                                        line_entry.file, line_entry.line, source_context, source_context, "->", strm)
138                                source_text = strm.GetData()
139                                if source_text:
140                                    # Indent the source a bit
141                                    indent_str = '    '
142                                    join_str = '\n' + indent_str
143                                    print('%s%s' % (indent_str, join_str.join(source_text.split('\n'))))
144                        if symbolicated_frame_address_idx == 0:
145                            if disassemble:
146                                instructions = symbolicated_frame_address.get_instructions()
147                                if instructions:
148                                    print()
149                                    symbolication.disassemble_instructions(
150                                        crash_log.get_target(),
151                                        instructions,
152                                        frame.pc,
153                                        options.disassemble_before,
154                                        options.disassemble_after,
155                                        frame.index > 0)
156                                    print()
157                        symbolicated_frame_address_idx += 1
158                else:
159                    print(frame)
160            if self.registers:
161                print()
162                for reg in self.registers.keys():
163                    print("    %-8s = %#16.16x" % (reg, self.registers[reg]))
164            elif self.crashed:
165               print()
166               print("No thread state (register information) available")
167
168        def add_ident(self, ident):
169            if ident not in self.idents:
170                self.idents.append(ident)
171
172        def did_crash(self):
173            return self.reason is not None
174
175        def __str__(self):
176            if self.app_specific_backtrace:
177                s = "Application Specific Backtrace[%u]" % self.index
178            else:
179                s = "Thread[%u]" % self.index
180            if self.reason:
181                s += ' %s' % self.reason
182            return s
183
184    class Frame:
185        """Class that represents a stack frame in a thread in a darwin crash log"""
186
187        def __init__(self, index, pc, description):
188            self.pc = pc
189            self.description = description
190            self.index = index
191
192        def __str__(self):
193            if self.description:
194                return "[%3u] 0x%16.16x %s" % (
195                    self.index, self.pc, self.description)
196            else:
197                return "[%3u] 0x%16.16x" % (self.index, self.pc)
198
199        def dump(self, prefix):
200            print("%s%s" % (prefix, str(self)))
201
202    class DarwinImage(symbolication.Image):
203        """Class that represents a binary images in a darwin crash log"""
204        dsymForUUIDBinary = '/usr/local/bin/dsymForUUID'
205        if not os.path.exists(dsymForUUIDBinary):
206            try:
207                dsymForUUIDBinary = subprocess.check_output('which dsymForUUID',
208                                                            shell=True).decode("utf-8").rstrip('\n')
209            except:
210                dsymForUUIDBinary = ""
211
212        dwarfdump_uuid_regex = re.compile(
213            'UUID: ([-0-9a-fA-F]+) \(([^\(]+)\) .*')
214
215        def __init__(
216                self,
217                text_addr_lo,
218                text_addr_hi,
219                identifier,
220                version,
221                uuid,
222                path,
223                verbose):
224            symbolication.Image.__init__(self, path, uuid)
225            self.add_section(
226                symbolication.Section(
227                    text_addr_lo,
228                    text_addr_hi,
229                    "__TEXT"))
230            self.identifier = identifier
231            self.version = version
232            self.verbose = verbose
233
234        def show_symbol_progress(self):
235            """
236            Hide progress output and errors from system frameworks as they are plentiful.
237            """
238            if self.verbose:
239                return True
240            return not (self.path.startswith("/System/Library/") or
241                        self.path.startswith("/usr/lib/"))
242
243
244        def find_matching_slice(self):
245            dwarfdump_cmd_output = subprocess.check_output(
246                'dwarfdump --uuid "%s"' % self.path, shell=True).decode("utf-8")
247            self_uuid = self.get_uuid()
248            for line in dwarfdump_cmd_output.splitlines():
249                match = self.dwarfdump_uuid_regex.search(line)
250                if match:
251                    dwarf_uuid_str = match.group(1)
252                    dwarf_uuid = uuid.UUID(dwarf_uuid_str)
253                    if self_uuid == dwarf_uuid:
254                        self.resolved_path = self.path
255                        self.arch = match.group(2)
256                        return True
257            if not self.resolved_path:
258                self.unavailable = True
259                if self.show_symbol_progress():
260                    print(("error\n    error: unable to locate '%s' with UUID %s"
261                           % (self.path, self.get_normalized_uuid_string())))
262                return False
263
264        def locate_module_and_debug_symbols(self):
265            # Don't load a module twice...
266            if self.resolved:
267                return True
268            # Mark this as resolved so we don't keep trying
269            self.resolved = True
270            uuid_str = self.get_normalized_uuid_string()
271            if self.show_symbol_progress():
272                print('Getting symbols for %s %s...' % (uuid_str, self.path), end=' ')
273            if os.path.exists(self.dsymForUUIDBinary):
274                dsym_for_uuid_command = '%s %s' % (
275                    self.dsymForUUIDBinary, uuid_str)
276                s = subprocess.check_output(dsym_for_uuid_command, shell=True)
277                if s:
278                    try:
279                        plist_root = read_plist(s)
280                    except:
281                        print(("Got exception: ", sys.exc_info()[1], " handling dsymForUUID output: \n", s))
282                        raise
283                    if plist_root:
284                        plist = plist_root[uuid_str]
285                        if plist:
286                            if 'DBGArchitecture' in plist:
287                                self.arch = plist['DBGArchitecture']
288                            if 'DBGDSYMPath' in plist:
289                                self.symfile = os.path.realpath(
290                                    plist['DBGDSYMPath'])
291                            if 'DBGSymbolRichExecutable' in plist:
292                                self.path = os.path.expanduser(
293                                    plist['DBGSymbolRichExecutable'])
294                                self.resolved_path = self.path
295            if not self.resolved_path and os.path.exists(self.path):
296                if not self.find_matching_slice():
297                    return False
298            if not self.resolved_path and not os.path.exists(self.path):
299                try:
300                    mdfind_results = subprocess.check_output(
301                        ["/usr/bin/mdfind",
302                         "com_apple_xcode_dsym_uuids == %s" % uuid_str]).decode("utf-8").splitlines()
303                    found_matching_slice = False
304                    for dsym in mdfind_results:
305                        dwarf_dir = os.path.join(dsym, 'Contents/Resources/DWARF')
306                        if not os.path.exists(dwarf_dir):
307                            # Not a dSYM bundle, probably an Xcode archive.
308                            continue
309                        print('falling back to binary inside "%s"' % dsym)
310                        self.symfile = dsym
311                        for filename in os.listdir(dwarf_dir):
312                           self.path = os.path.join(dwarf_dir, filename)
313                           if self.find_matching_slice():
314                              found_matching_slice = True
315                              break
316                        if found_matching_slice:
317                           break
318                except:
319                    pass
320            if (self.resolved_path and os.path.exists(self.resolved_path)) or (
321                    self.path and os.path.exists(self.path)):
322                print('ok')
323                return True
324            else:
325                self.unavailable = True
326            return False
327
328    def __init__(self, debugger, path, verbose):
329        """CrashLog constructor that take a path to a darwin crash log file"""
330        symbolication.Symbolicator.__init__(self, debugger)
331        self.path = os.path.expanduser(path)
332        self.info_lines = list()
333        self.system_profile = list()
334        self.threads = list()
335        self.backtraces = list()  # For application specific backtraces
336        self.idents = list()  # A list of the required identifiers for doing all stack backtraces
337        self.crashed_thread_idx = -1
338        self.version = -1
339        self.target = None
340        self.verbose = verbose
341
342    def dump(self):
343        print("Crash Log File: %s" % (self.path))
344        if self.backtraces:
345            print("\nApplication Specific Backtraces:")
346            for thread in self.backtraces:
347                thread.dump('  ')
348        print("\nThreads:")
349        for thread in self.threads:
350            thread.dump('  ')
351        print("\nImages:")
352        for image in self.images:
353            image.dump('  ')
354
355    def find_image_with_identifier(self, identifier):
356        for image in self.images:
357            if image.identifier == identifier:
358                return image
359        regex_text = '^.*\.%s$' % (re.escape(identifier))
360        regex = re.compile(regex_text)
361        for image in self.images:
362            if regex.match(image.identifier):
363                return image
364        return None
365
366    def create_target(self):
367        if self.target is None:
368            self.target = symbolication.Symbolicator.create_target(self)
369            if self.target:
370                return self.target
371            # We weren't able to open the main executable as, but we can still
372            # symbolicate
373            print('crashlog.create_target()...2')
374            if self.idents:
375                for ident in self.idents:
376                    image = self.find_image_with_identifier(ident)
377                    if image:
378                        self.target = image.create_target(self.debugger)
379                        if self.target:
380                            return self.target  # success
381            print('crashlog.create_target()...3')
382            for image in self.images:
383                self.target = image.create_target(self.debugger)
384                if self.target:
385                    return self.target  # success
386            print('crashlog.create_target()...4')
387            print('error: Unable to locate any executables from the crash log.')
388            print('       Try loading the executable into lldb before running crashlog')
389            print('       and/or make sure the .dSYM bundles can be found by Spotlight.')
390        return self.target
391
392    def get_target(self):
393        return self.target
394
395
396class CrashLogFormatException(Exception):
397    pass
398
399
400class CrashLogParseException(Exception):
401   pass
402
403
404class CrashLogParser:
405    def parse(self, debugger, path, verbose):
406        try:
407            return JSONCrashLogParser(debugger, path, verbose).parse()
408        except CrashLogFormatException:
409            return TextCrashLogParser(debugger, path, verbose).parse()
410
411
412class JSONCrashLogParser:
413    def __init__(self, debugger, path, verbose):
414        self.path = os.path.expanduser(path)
415        self.verbose = verbose
416        self.crashlog = CrashLog(debugger, self.path, self.verbose)
417
418    def parse(self):
419        with open(self.path, 'r') as f:
420            buffer = f.read()
421
422        # Skip the first line if it contains meta data.
423        head, _, tail = buffer.partition('\n')
424        try:
425            metadata = json.loads(head)
426            if 'app_name' in metadata and 'app_version' in metadata:
427                buffer = tail
428        except ValueError:
429            pass
430
431        try:
432            self.data = json.loads(buffer)
433        except ValueError:
434            raise CrashLogFormatException()
435
436        try:
437            self.parse_process_info(self.data)
438            self.parse_images(self.data['usedImages'])
439            self.parse_threads(self.data['threads'])
440            thread = self.crashlog.threads[self.crashlog.crashed_thread_idx]
441            reason = self.parse_crash_reason(self.data['exception'])
442            if thread.reason:
443                thread.reason = '{} {}'.format(thread.reason, reason)
444            else:
445                thread.reason = reason
446        except (KeyError, ValueError, TypeError) as e:
447            raise CrashLogParseException(
448                'Failed to parse JSON crashlog: {}: {}'.format(
449                    type(e).__name__, e))
450
451        return self.crashlog
452
453    def get_used_image(self, idx):
454        return self.data['usedImages'][idx]
455
456    def parse_process_info(self, json_data):
457        self.crashlog.process_id = json_data['pid']
458        self.crashlog.process_identifier = json_data['procName']
459        self.crashlog.process_path = json_data['procPath']
460
461    def parse_crash_reason(self, json_exception):
462        exception_type = json_exception['type']
463        exception_signal = json_exception['signal']
464        if 'codes' in json_exception:
465            exception_extra = " ({})".format(json_exception['codes'])
466        elif 'subtype' in json_exception:
467            exception_extra = " ({})".format(json_exception['subtype'])
468        else:
469            exception_extra = ""
470        return "{} ({}){}".format(exception_type, exception_signal,
471                                  exception_extra)
472
473    def parse_images(self, json_images):
474        idx = 0
475        for json_image in json_images:
476            img_uuid = uuid.UUID(json_image['uuid'])
477            low = int(json_image['base'])
478            high = int(0)
479            name = json_image['name'] if 'name' in json_image else ''
480            path = json_image['path'] if 'path' in json_image else ''
481            version = ''
482            darwin_image = self.crashlog.DarwinImage(low, high, name, version,
483                                                     img_uuid, path,
484                                                     self.verbose)
485            self.crashlog.images.append(darwin_image)
486            idx += 1
487
488    def parse_frames(self, thread, json_frames):
489        idx = 0
490        for json_frame in json_frames:
491            image_id = int(json_frame['imageIndex'])
492            json_image = self.get_used_image(image_id)
493            ident = json_image['name'] if 'name' in json_image else ''
494            thread.add_ident(ident)
495            if ident not in self.crashlog.idents:
496                self.crashlog.idents.append(ident)
497
498            frame_offset = int(json_frame['imageOffset'])
499            image_addr = self.get_used_image(image_id)['base']
500            pc = image_addr + frame_offset
501            thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset))
502            idx += 1
503
504    def parse_threads(self, json_threads):
505        idx = 0
506        for json_thread in json_threads:
507            thread = self.crashlog.Thread(idx, False)
508            if 'name' in json_thread:
509                thread.reason = json_thread['name']
510            if json_thread.get('triggered', False):
511                self.crashlog.crashed_thread_idx = idx
512                thread.crashed = True
513                if 'threadState' in json_thread:
514                    thread.registers = self.parse_thread_registers(
515                        json_thread['threadState'])
516            thread.queue = json_thread.get('queue')
517            self.parse_frames(thread, json_thread.get('frames', []))
518            self.crashlog.threads.append(thread)
519            idx += 1
520
521    def parse_thread_registers(self, json_thread_state):
522        registers = dict()
523        for key, state in json_thread_state.items():
524            try:
525               value = int(state['value'])
526               registers[key] = value
527            except (TypeError, ValueError):
528               pass
529        return registers
530
531
532class CrashLogParseMode:
533    NORMAL = 0
534    THREAD = 1
535    IMAGES = 2
536    THREGS = 3
537    SYSTEM = 4
538    INSTRS = 5
539
540
541class TextCrashLogParser:
542    parent_process_regex = re.compile('^Parent Process:\s*(.*)\[(\d+)\]')
543    thread_state_regex = re.compile('^Thread ([0-9]+) crashed with')
544    thread_instrs_regex = re.compile('^Thread ([0-9]+) instruction stream')
545    thread_regex = re.compile('^Thread ([0-9]+)([^:]*):(.*)')
546    app_backtrace_regex = re.compile('^Application Specific Backtrace ([0-9]+)([^:]*):(.*)')
547    version = r'(\(.+\)|(arm|x86_)[0-9a-z]+)\s+'
548    frame_regex = re.compile(r'^([0-9]+)' r'\s'                # id
549                             r'+(.+?)'    r'\s+'               # img_name
550                             r'(' +version+ r')?'              # img_version
551                             r'(0x[0-9a-fA-F]{7}[0-9a-fA-F]+)' # addr
552                             r' +(.*)'                         # offs
553                            )
554    null_frame_regex = re.compile(r'^([0-9]+)\s+\?\?\?\s+(0{7}0+) +(.*)')
555    image_regex_uuid = re.compile(r'(0x[0-9a-fA-F]+)'            # img_lo
556                                  r'\s+' '-' r'\s+'              #   -
557                                  r'(0x[0-9a-fA-F]+)'     r'\s+' # img_hi
558                                  r'[+]?(.+?)'            r'\s+' # img_name
559                                  r'(' +version+ ')?'            # img_version
560                                  r'(<([-0-9a-fA-F]+)>\s+)?'     # img_uuid
561                                  r'(/.*)'                       # img_path
562                                 )
563
564
565    def __init__(self, debugger, path, verbose):
566        self.path = os.path.expanduser(path)
567        self.verbose = verbose
568        self.thread = None
569        self.app_specific_backtrace = False
570        self.crashlog = CrashLog(debugger, self.path, self.verbose)
571        self.parse_mode = CrashLogParseMode.NORMAL
572        self.parsers = {
573            CrashLogParseMode.NORMAL : self.parse_normal,
574            CrashLogParseMode.THREAD : self.parse_thread,
575            CrashLogParseMode.IMAGES : self.parse_images,
576            CrashLogParseMode.THREGS : self.parse_thread_registers,
577            CrashLogParseMode.SYSTEM : self.parse_system,
578            CrashLogParseMode.INSTRS : self.parse_instructions,
579        }
580
581    def parse(self):
582        with open(self.path,'r') as f:
583            lines = f.read().splitlines()
584
585        for line in lines:
586            line_len = len(line)
587            if line_len == 0:
588                if self.thread:
589                    if self.parse_mode == CrashLogParseMode.THREAD:
590                        if self.thread.index == self.crashlog.crashed_thread_idx:
591                            self.thread.reason = ''
592                            if self.crashlog.thread_exception:
593                                self.thread.reason += self.crashlog.thread_exception
594                            if self.crashlog.thread_exception_data:
595                                self.thread.reason += " (%s)" % self.crashlog.thread_exception_data
596                        if self.app_specific_backtrace:
597                            self.crashlog.backtraces.append(self.thread)
598                        else:
599                            self.crashlog.threads.append(self.thread)
600                    self.thread = None
601                else:
602                    # only append an extra empty line if the previous line
603                    # in the info_lines wasn't empty
604                    if len(self.crashlog.info_lines) > 0 and len(self.crashlog.info_lines[-1]):
605                        self.crashlog.info_lines.append(line)
606                self.parse_mode = CrashLogParseMode.NORMAL
607            else:
608                self.parsers[self.parse_mode](line)
609
610        return self.crashlog
611
612
613    def parse_normal(self, line):
614        if line.startswith('Process:'):
615            (self.crashlog.process_name, pid_with_brackets) = line[
616                8:].strip().split(' [')
617            self.crashlog.process_id = pid_with_brackets.strip('[]')
618        elif line.startswith('Path:'):
619            self.crashlog.process_path = line[5:].strip()
620        elif line.startswith('Identifier:'):
621            self.crashlog.process_identifier = line[11:].strip()
622        elif line.startswith('Version:'):
623            version_string = line[8:].strip()
624            matched_pair = re.search("(.+)\((.+)\)", version_string)
625            if matched_pair:
626                self.crashlog.process_version = matched_pair.group(1)
627                self.crashlog.process_compatability_version = matched_pair.group(
628                    2)
629            else:
630                self.crashlog.process = version_string
631                self.crashlog.process_compatability_version = version_string
632        elif self.parent_process_regex.search(line):
633            parent_process_match = self.parent_process_regex.search(
634                line)
635            self.crashlog.parent_process_name = parent_process_match.group(1)
636            self.crashlog.parent_process_id = parent_process_match.group(2)
637        elif line.startswith('Exception Type:'):
638            self.crashlog.thread_exception = line[15:].strip()
639            return
640        elif line.startswith('Exception Codes:'):
641            self.crashlog.thread_exception_data = line[16:].strip()
642            return
643        elif line.startswith('Exception Subtype:'): # iOS
644            self.crashlog.thread_exception_data = line[18:].strip()
645            return
646        elif line.startswith('Crashed Thread:'):
647            self.crashlog.crashed_thread_idx = int(line[15:].strip().split()[0])
648            return
649        elif line.startswith('Triggered by Thread:'): # iOS
650            self.crashlog.crashed_thread_idx = int(line[20:].strip().split()[0])
651            return
652        elif line.startswith('Report Version:'):
653            self.crashlog.version = int(line[15:].strip())
654            return
655        elif line.startswith('System Profile:'):
656            self.parse_mode = CrashLogParseMode.SYSTEM
657            return
658        elif (line.startswith('Interval Since Last Report:') or
659                line.startswith('Crashes Since Last Report:') or
660                line.startswith('Per-App Interval Since Last Report:') or
661                line.startswith('Per-App Crashes Since Last Report:') or
662                line.startswith('Sleep/Wake UUID:') or
663                line.startswith('Anonymous UUID:')):
664            # ignore these
665            return
666        elif line.startswith('Thread'):
667            thread_state_match = self.thread_state_regex.search(line)
668            if thread_state_match:
669                self.app_specific_backtrace = False
670                thread_state_match = self.thread_regex.search(line)
671                thread_idx = int(thread_state_match.group(1))
672                self.parse_mode = CrashLogParseMode.THREGS
673                self.thread = self.crashlog.threads[thread_idx]
674                return
675            thread_insts_match  = self.thread_instrs_regex.search(line)
676            if thread_insts_match:
677                self.parse_mode = CrashLogParseMode.INSTRS
678                return
679            thread_match = self.thread_regex.search(line)
680            if thread_match:
681                self.app_specific_backtrace = False
682                self.parse_mode = CrashLogParseMode.THREAD
683                thread_idx = int(thread_match.group(1))
684                self.thread = self.crashlog.Thread(thread_idx, False)
685                return
686            return
687        elif line.startswith('Binary Images:'):
688            self.parse_mode = CrashLogParseMode.IMAGES
689            return
690        elif line.startswith('Application Specific Backtrace'):
691            app_backtrace_match = self.app_backtrace_regex.search(line)
692            if app_backtrace_match:
693                self.parse_mode = CrashLogParseMode.THREAD
694                self.app_specific_backtrace = True
695                idx = int(app_backtrace_match.group(1))
696                self.thread = self.crashlog.Thread(idx, True)
697        elif line.startswith('Last Exception Backtrace:'): # iOS
698            self.parse_mode = CrashLogParseMode.THREAD
699            self.app_specific_backtrace = True
700            idx = 1
701            self.thread = self.crashlog.Thread(idx, True)
702        self.crashlog.info_lines.append(line.strip())
703
704    def parse_thread(self, line):
705        if line.startswith('Thread'):
706            return
707        if self.null_frame_regex.search(line):
708            print('warning: thread parser ignored null-frame: "%s"' % line)
709            return
710        frame_match = self.frame_regex.search(line)
711        if frame_match:
712            (frame_id, frame_img_name, _, frame_img_version, _,
713                frame_addr, frame_ofs) = frame_match.groups()
714            ident = frame_img_name
715            self.thread.add_ident(ident)
716            if ident not in self.crashlog.idents:
717                self.crashlog.idents.append(ident)
718            self.thread.frames.append(self.crashlog.Frame(int(frame_id), int(
719                frame_addr, 0), frame_ofs))
720        else:
721            print('error: frame regex failed for line: "%s"' % line)
722
723    def parse_images(self, line):
724        image_match = self.image_regex_uuid.search(line)
725        if image_match:
726            (img_lo, img_hi, img_name, _, img_version, _,
727                _, img_uuid, img_path) = image_match.groups()
728            image = self.crashlog.DarwinImage(int(img_lo, 0), int(img_hi, 0),
729                                            img_name.strip(),
730                                            img_version.strip()
731                                            if img_version else "",
732                                            uuid.UUID(img_uuid), img_path,
733                                            self.verbose)
734            self.crashlog.images.append(image)
735        else:
736            print("error: image regex failed for: %s" % line)
737
738
739    def parse_thread_registers(self, line):
740        stripped_line = line.strip()
741        # "r12: 0x00007fff6b5939c8  r13: 0x0000000007000006  r14: 0x0000000000002a03  r15: 0x0000000000000c00"
742        reg_values = re.findall(
743            '([a-zA-Z0-9]+: 0[Xx][0-9a-fA-F]+) *', stripped_line)
744        for reg_value in reg_values:
745            (reg, value) = reg_value.split(': ')
746            self.thread.registers[reg.strip()] = int(value, 0)
747
748    def parse_system(self, line):
749        self.crashlog.system_profile.append(line)
750
751    def parse_instructions(self, line):
752        pass
753
754
755def usage():
756    print("Usage: lldb-symbolicate.py [-n name] executable-image")
757    sys.exit(0)
758
759
760class Interactive(cmd.Cmd):
761    '''Interactive prompt for analyzing one or more Darwin crash logs, type "help" to see a list of supported commands.'''
762    image_option_parser = None
763
764    def __init__(self, crash_logs):
765        cmd.Cmd.__init__(self)
766        self.use_rawinput = False
767        self.intro = 'Interactive crashlogs prompt, type "help" to see a list of supported commands.'
768        self.crash_logs = crash_logs
769        self.prompt = '% '
770
771    def default(self, line):
772        '''Catch all for unknown command, which will exit the interpreter.'''
773        print("uknown command: %s" % line)
774        return True
775
776    def do_q(self, line):
777        '''Quit command'''
778        return True
779
780    def do_quit(self, line):
781        '''Quit command'''
782        return True
783
784    def do_symbolicate(self, line):
785        description = '''Symbolicate one or more darwin crash log files by index to provide source file and line information,
786        inlined stack frames back to the concrete functions, and disassemble the location of the crash
787        for the first frame of the crashed thread.'''
788        option_parser = CreateSymbolicateCrashLogOptions(
789            'symbolicate', description, False)
790        command_args = shlex.split(line)
791        try:
792            (options, args) = option_parser.parse_args(command_args)
793        except:
794            return
795
796        if args:
797            # We have arguments, they must valid be crash log file indexes
798            for idx_str in args:
799                idx = int(idx_str)
800                if idx < len(self.crash_logs):
801                    SymbolicateCrashLog(self.crash_logs[idx], options)
802                else:
803                    print('error: crash log index %u is out of range' % (idx))
804        else:
805            # No arguments, symbolicate all crash logs using the options
806            # provided
807            for idx in range(len(self.crash_logs)):
808                SymbolicateCrashLog(self.crash_logs[idx], options)
809
810    def do_list(self, line=None):
811        '''Dump a list of all crash logs that are currently loaded.
812
813        USAGE: list'''
814        print('%u crash logs are loaded:' % len(self.crash_logs))
815        for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
816            print('[%u] = %s' % (crash_log_idx, crash_log.path))
817
818    def do_image(self, line):
819        '''Dump information about one or more binary images in the crash log given an image basename, or all images if no arguments are provided.'''
820        usage = "usage: %prog [options] <PATH> [PATH ...]"
821        description = '''Dump information about one or more images in all crash logs. The <PATH> can be a full path, image basename, or partial path. Searches are done in this order.'''
822        command_args = shlex.split(line)
823        if not self.image_option_parser:
824            self.image_option_parser = optparse.OptionParser(
825                description=description, prog='image', usage=usage)
826            self.image_option_parser.add_option(
827                '-a',
828                '--all',
829                action='store_true',
830                help='show all images',
831                default=False)
832        try:
833            (options, args) = self.image_option_parser.parse_args(command_args)
834        except:
835            return
836
837        if args:
838            for image_path in args:
839                fullpath_search = image_path[0] == '/'
840                for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
841                    matches_found = 0
842                    for (image_idx, image) in enumerate(crash_log.images):
843                        if fullpath_search:
844                            if image.get_resolved_path() == image_path:
845                                matches_found += 1
846                                print('[%u] ' % (crash_log_idx), image)
847                        else:
848                            image_basename = image.get_resolved_path_basename()
849                            if image_basename == image_path:
850                                matches_found += 1
851                                print('[%u] ' % (crash_log_idx), image)
852                    if matches_found == 0:
853                        for (image_idx, image) in enumerate(crash_log.images):
854                            resolved_image_path = image.get_resolved_path()
855                            if resolved_image_path and string.find(
856                                    image.get_resolved_path(), image_path) >= 0:
857                                print('[%u] ' % (crash_log_idx), image)
858        else:
859            for crash_log in self.crash_logs:
860                for (image_idx, image) in enumerate(crash_log.images):
861                    print('[%u] %s' % (image_idx, image))
862        return False
863
864
865def interactive_crashlogs(debugger, options, args):
866    crash_log_files = list()
867    for arg in args:
868        for resolved_path in glob.glob(arg):
869            crash_log_files.append(resolved_path)
870
871    crash_logs = list()
872    for crash_log_file in crash_log_files:
873        try:
874            crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose)
875        except Exception as e:
876            print(e)
877            continue
878        if options.debug:
879            crash_log.dump()
880        if not crash_log.images:
881            print('error: no images in crash log "%s"' % (crash_log))
882            continue
883        else:
884            crash_logs.append(crash_log)
885
886    interpreter = Interactive(crash_logs)
887    # List all crash logs that were imported
888    interpreter.do_list()
889    interpreter.cmdloop()
890
891
892def save_crashlog(debugger, command, exe_ctx, result, dict):
893    usage = "usage: %prog [options] <output-path>"
894    description = '''Export the state of current target into a crashlog file'''
895    parser = optparse.OptionParser(
896        description=description,
897        prog='save_crashlog',
898        usage=usage)
899    parser.add_option(
900        '-v',
901        '--verbose',
902        action='store_true',
903        dest='verbose',
904        help='display verbose debug info',
905        default=False)
906    try:
907        (options, args) = parser.parse_args(shlex.split(command))
908    except:
909        result.PutCString("error: invalid options")
910        return
911    if len(args) != 1:
912        result.PutCString(
913            "error: invalid arguments, a single output file is the only valid argument")
914        return
915    out_file = open(args[0], 'w')
916    if not out_file:
917        result.PutCString(
918            "error: failed to open file '%s' for writing...",
919            args[0])
920        return
921    target = exe_ctx.target
922    if target:
923        identifier = target.executable.basename
924        process = exe_ctx.process
925        if process:
926            pid = process.id
927            if pid != lldb.LLDB_INVALID_PROCESS_ID:
928                out_file.write(
929                    'Process:         %s [%u]\n' %
930                    (identifier, pid))
931        out_file.write('Path:            %s\n' % (target.executable.fullpath))
932        out_file.write('Identifier:      %s\n' % (identifier))
933        out_file.write('\nDate/Time:       %s\n' %
934                       (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
935        out_file.write(
936            'OS Version:      Mac OS X %s (%s)\n' %
937            (platform.mac_ver()[0], subprocess.check_output('sysctl -n kern.osversion', shell=True).decode("utf-8")))
938        out_file.write('Report Version:  9\n')
939        for thread_idx in range(process.num_threads):
940            thread = process.thread[thread_idx]
941            out_file.write('\nThread %u:\n' % (thread_idx))
942            for (frame_idx, frame) in enumerate(thread.frames):
943                frame_pc = frame.pc
944                frame_offset = 0
945                if frame.function:
946                    block = frame.GetFrameBlock()
947                    block_range = block.range[frame.addr]
948                    if block_range:
949                        block_start_addr = block_range[0]
950                        frame_offset = frame_pc - block_start_addr.GetLoadAddress(target)
951                    else:
952                        frame_offset = frame_pc - frame.function.addr.GetLoadAddress(target)
953                elif frame.symbol:
954                    frame_offset = frame_pc - frame.symbol.addr.GetLoadAddress(target)
955                out_file.write(
956                    '%-3u %-32s 0x%16.16x %s' %
957                    (frame_idx, frame.module.file.basename, frame_pc, frame.name))
958                if frame_offset > 0:
959                    out_file.write(' + %u' % (frame_offset))
960                line_entry = frame.line_entry
961                if line_entry:
962                    if options.verbose:
963                        # This will output the fullpath + line + column
964                        out_file.write(' %s' % (line_entry))
965                    else:
966                        out_file.write(
967                            ' %s:%u' %
968                            (line_entry.file.basename, line_entry.line))
969                        column = line_entry.column
970                        if column:
971                            out_file.write(':%u' % (column))
972                out_file.write('\n')
973
974        out_file.write('\nBinary Images:\n')
975        for module in target.modules:
976            text_segment = module.section['__TEXT']
977            if text_segment:
978                text_segment_load_addr = text_segment.GetLoadAddress(target)
979                if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS:
980                    text_segment_end_load_addr = text_segment_load_addr + text_segment.size
981                    identifier = module.file.basename
982                    module_version = '???'
983                    module_version_array = module.GetVersion()
984                    if module_version_array:
985                        module_version = '.'.join(
986                            map(str, module_version_array))
987                    out_file.write(
988                        '    0x%16.16x - 0x%16.16x  %s (%s - ???) <%s> %s\n' %
989                        (text_segment_load_addr,
990                         text_segment_end_load_addr,
991                         identifier,
992                         module_version,
993                         module.GetUUIDString(),
994                         module.file.fullpath))
995        out_file.close()
996    else:
997        result.PutCString("error: invalid target")
998
999
1000def Symbolicate(debugger, command, result, dict):
1001    try:
1002        SymbolicateCrashLogs(debugger, shlex.split(command))
1003    except Exception as e:
1004        result.PutCString("error: python exception: %s" % e)
1005
1006
1007def SymbolicateCrashLog(crash_log, options):
1008    if options.debug:
1009        crash_log.dump()
1010    if not crash_log.images:
1011        print('error: no images in crash log')
1012        return
1013
1014    if options.dump_image_list:
1015        print("Binary Images:")
1016        for image in crash_log.images:
1017            if options.verbose:
1018                print(image.debug_dump())
1019            else:
1020                print(image)
1021
1022    target = crash_log.create_target()
1023    if not target:
1024        return
1025    exe_module = target.GetModuleAtIndex(0)
1026    images_to_load = list()
1027    loaded_images = list()
1028    if options.load_all_images:
1029        # --load-all option was specified, load everything up
1030        for image in crash_log.images:
1031            images_to_load.append(image)
1032    else:
1033        # Only load the images found in stack frames for the crashed threads
1034        if options.crashed_only:
1035            for thread in crash_log.threads:
1036                if thread.did_crash():
1037                    for ident in thread.idents:
1038                        images = crash_log.find_images_with_identifier(ident)
1039                        if images:
1040                            for image in images:
1041                                images_to_load.append(image)
1042                        else:
1043                            print('error: can\'t find image for identifier "%s"' % ident)
1044        else:
1045            for ident in crash_log.idents:
1046                images = crash_log.find_images_with_identifier(ident)
1047                if images:
1048                    for image in images:
1049                        images_to_load.append(image)
1050                else:
1051                    print('error: can\'t find image for identifier "%s"' % ident)
1052
1053    for image in images_to_load:
1054        if image not in loaded_images:
1055            err = image.add_module(target)
1056            if err:
1057                print(err)
1058            else:
1059                loaded_images.append(image)
1060
1061    if crash_log.backtraces:
1062        for thread in crash_log.backtraces:
1063            thread.dump_symbolicated(crash_log, options)
1064            print()
1065
1066    for thread in crash_log.threads:
1067        thread.dump_symbolicated(crash_log, options)
1068        print()
1069
1070
1071def CreateSymbolicateCrashLogOptions(
1072        command_name,
1073        description,
1074        add_interactive_options):
1075    usage = "usage: %prog [options] <FILE> [FILE ...]"
1076    option_parser = optparse.OptionParser(
1077        description=description, prog='crashlog', usage=usage)
1078    option_parser.add_option(
1079        '--verbose',
1080        '-v',
1081        action='store_true',
1082        dest='verbose',
1083        help='display verbose debug info',
1084        default=False)
1085    option_parser.add_option(
1086        '--debug',
1087        '-g',
1088        action='store_true',
1089        dest='debug',
1090        help='display verbose debug logging',
1091        default=False)
1092    option_parser.add_option(
1093        '--load-all',
1094        '-a',
1095        action='store_true',
1096        dest='load_all_images',
1097        help='load all executable images, not just the images found in the crashed stack frames',
1098        default=False)
1099    option_parser.add_option(
1100        '--images',
1101        action='store_true',
1102        dest='dump_image_list',
1103        help='show image list',
1104        default=False)
1105    option_parser.add_option(
1106        '--debug-delay',
1107        type='int',
1108        dest='debug_delay',
1109        metavar='NSEC',
1110        help='pause for NSEC seconds for debugger',
1111        default=0)
1112    option_parser.add_option(
1113        '--crashed-only',
1114        '-c',
1115        action='store_true',
1116        dest='crashed_only',
1117        help='only symbolicate the crashed thread',
1118        default=False)
1119    option_parser.add_option(
1120        '--disasm-depth',
1121        '-d',
1122        type='int',
1123        dest='disassemble_depth',
1124        help='set the depth in stack frames that should be disassembled (default is 1)',
1125        default=1)
1126    option_parser.add_option(
1127        '--disasm-all',
1128        '-D',
1129        action='store_true',
1130        dest='disassemble_all_threads',
1131        help='enabled disassembly of frames on all threads (not just the crashed thread)',
1132        default=False)
1133    option_parser.add_option(
1134        '--disasm-before',
1135        '-B',
1136        type='int',
1137        dest='disassemble_before',
1138        help='the number of instructions to disassemble before the frame PC',
1139        default=4)
1140    option_parser.add_option(
1141        '--disasm-after',
1142        '-A',
1143        type='int',
1144        dest='disassemble_after',
1145        help='the number of instructions to disassemble after the frame PC',
1146        default=4)
1147    option_parser.add_option(
1148        '--source-context',
1149        '-C',
1150        type='int',
1151        metavar='NLINES',
1152        dest='source_context',
1153        help='show NLINES source lines of source context (default = 4)',
1154        default=4)
1155    option_parser.add_option(
1156        '--source-frames',
1157        type='int',
1158        metavar='NFRAMES',
1159        dest='source_frames',
1160        help='show source for NFRAMES (default = 4)',
1161        default=4)
1162    option_parser.add_option(
1163        '--source-all',
1164        action='store_true',
1165        dest='source_all',
1166        help='show source for all threads, not just the crashed thread',
1167        default=False)
1168    if add_interactive_options:
1169        option_parser.add_option(
1170            '-i',
1171            '--interactive',
1172            action='store_true',
1173            help='parse all crash logs and enter interactive mode',
1174            default=False)
1175    return option_parser
1176
1177
1178def SymbolicateCrashLogs(debugger, command_args):
1179    description = '''Symbolicate one or more darwin crash log files to provide source file and line information,
1180inlined stack frames back to the concrete functions, and disassemble the location of the crash
1181for the first frame of the crashed thread.
1182If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter
1183for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been
1184created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows
1185you to explore the program as if it were stopped at the locations described in the crash log and functions can
1186be disassembled and lookups can be performed using the addresses found in the crash log.'''
1187    option_parser = CreateSymbolicateCrashLogOptions(
1188        'crashlog', description, True)
1189    try:
1190        (options, args) = option_parser.parse_args(command_args)
1191    except:
1192        return
1193
1194    if options.debug:
1195        print('command_args = %s' % command_args)
1196        print('options', options)
1197        print('args', args)
1198
1199    if options.debug_delay > 0:
1200        print("Waiting %u seconds for debugger to attach..." % options.debug_delay)
1201        time.sleep(options.debug_delay)
1202    error = lldb.SBError()
1203
1204    if args:
1205        if options.interactive:
1206            interactive_crashlogs(debugger, options, args)
1207        else:
1208            for crash_log_file in args:
1209                crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose)
1210                SymbolicateCrashLog(crash_log, options)
1211if __name__ == '__main__':
1212    # Create a new debugger instance
1213    debugger = lldb.SBDebugger.Create()
1214    SymbolicateCrashLogs(debugger, sys.argv[1:])
1215    lldb.SBDebugger.Destroy(debugger)
1216elif getattr(lldb, 'debugger', None):
1217    lldb.debugger.HandleCommand(
1218        'command script add -f lldb.macosx.crashlog.Symbolicate crashlog')
1219    lldb.debugger.HandleCommand(
1220        'command script add -f lldb.macosx.crashlog.save_crashlog save_crashlog')
1221