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.errors = list()
338        self.crashed_thread_idx = -1
339        self.version = -1
340        self.target = None
341        self.verbose = verbose
342
343    def dump(self):
344        print("Crash Log File: %s" % (self.path))
345        if self.backtraces:
346            print("\nApplication Specific Backtraces:")
347            for thread in self.backtraces:
348                thread.dump('  ')
349        print("\nThreads:")
350        for thread in self.threads:
351            thread.dump('  ')
352        print("\nImages:")
353        for image in self.images:
354            image.dump('  ')
355
356    def find_image_with_identifier(self, identifier):
357        for image in self.images:
358            if image.identifier == identifier:
359                return image
360        regex_text = '^.*\.%s$' % (re.escape(identifier))
361        regex = re.compile(regex_text)
362        for image in self.images:
363            if regex.match(image.identifier):
364                return image
365        return None
366
367    def create_target(self):
368        if self.target is None:
369            self.target = symbolication.Symbolicator.create_target(self)
370            if self.target:
371                return self.target
372            # We weren't able to open the main executable as, but we can still
373            # symbolicate
374            print('crashlog.create_target()...2')
375            if self.idents:
376                for ident in self.idents:
377                    image = self.find_image_with_identifier(ident)
378                    if image:
379                        self.target = image.create_target(self.debugger)
380                        if self.target:
381                            return self.target  # success
382            print('crashlog.create_target()...3')
383            for image in self.images:
384                self.target = image.create_target(self.debugger)
385                if self.target:
386                    return self.target  # success
387            print('crashlog.create_target()...4')
388            print('error: Unable to locate any executables from the crash log.')
389            print('       Try loading the executable into lldb before running crashlog')
390            print('       and/or make sure the .dSYM bundles can be found by Spotlight.')
391        return self.target
392
393    def get_target(self):
394        return self.target
395
396
397class CrashLogFormatException(Exception):
398    pass
399
400
401class CrashLogParseException(Exception):
402   pass
403
404
405class CrashLogParser:
406    def parse(self, debugger, path, verbose):
407        try:
408            return JSONCrashLogParser(debugger, path, verbose).parse()
409        except CrashLogFormatException:
410            return TextCrashLogParser(debugger, path, verbose).parse()
411
412
413class JSONCrashLogParser:
414    def __init__(self, debugger, path, verbose):
415        self.path = os.path.expanduser(path)
416        self.verbose = verbose
417        self.crashlog = CrashLog(debugger, self.path, self.verbose)
418
419    def parse(self):
420        with open(self.path, 'r') as f:
421            buffer = f.read()
422
423        # Skip the first line if it contains meta data.
424        head, _, tail = buffer.partition('\n')
425        try:
426            metadata = json.loads(head)
427            if 'app_name' in metadata and 'app_version' in metadata:
428                buffer = tail
429        except ValueError:
430            pass
431
432        try:
433            self.data = json.loads(buffer)
434        except ValueError:
435            raise CrashLogFormatException()
436
437        try:
438            self.parse_process_info(self.data)
439            self.parse_images(self.data['usedImages'])
440            self.parse_threads(self.data['threads'])
441            self.parse_errors(self.data)
442            thread = self.crashlog.threads[self.crashlog.crashed_thread_idx]
443            reason = self.parse_crash_reason(self.data['exception'])
444            if thread.reason:
445                thread.reason = '{} {}'.format(thread.reason, reason)
446            else:
447                thread.reason = reason
448        except (KeyError, ValueError, TypeError) as e:
449            raise CrashLogParseException(
450                'Failed to parse JSON crashlog: {}: {}'.format(
451                    type(e).__name__, e))
452
453        return self.crashlog
454
455    def get_used_image(self, idx):
456        return self.data['usedImages'][idx]
457
458    def parse_process_info(self, json_data):
459        self.crashlog.process_id = json_data['pid']
460        self.crashlog.process_identifier = json_data['procName']
461        self.crashlog.process_path = json_data['procPath']
462
463    def parse_crash_reason(self, json_exception):
464        exception_type = json_exception['type']
465        exception_signal = json_exception['signal']
466        if 'codes' in json_exception:
467            exception_extra = " ({})".format(json_exception['codes'])
468        elif 'subtype' in json_exception:
469            exception_extra = " ({})".format(json_exception['subtype'])
470        else:
471            exception_extra = ""
472        return "{} ({}){}".format(exception_type, exception_signal,
473                                  exception_extra)
474
475    def parse_images(self, json_images):
476        idx = 0
477        for json_image in json_images:
478            img_uuid = uuid.UUID(json_image['uuid'])
479            low = int(json_image['base'])
480            high = int(0)
481            name = json_image['name'] if 'name' in json_image else ''
482            path = json_image['path'] if 'path' in json_image else ''
483            version = ''
484            darwin_image = self.crashlog.DarwinImage(low, high, name, version,
485                                                     img_uuid, path,
486                                                     self.verbose)
487            self.crashlog.images.append(darwin_image)
488            idx += 1
489
490    def parse_frames(self, thread, json_frames):
491        idx = 0
492        for json_frame in json_frames:
493            image_id = int(json_frame['imageIndex'])
494            json_image = self.get_used_image(image_id)
495            ident = json_image['name'] if 'name' in json_image else ''
496            thread.add_ident(ident)
497            if ident not in self.crashlog.idents:
498                self.crashlog.idents.append(ident)
499
500            frame_offset = int(json_frame['imageOffset'])
501            image_addr = self.get_used_image(image_id)['base']
502            pc = image_addr + frame_offset
503            thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset))
504            idx += 1
505
506    def parse_threads(self, json_threads):
507        idx = 0
508        for json_thread in json_threads:
509            thread = self.crashlog.Thread(idx, False)
510            if 'name' in json_thread:
511                thread.reason = json_thread['name']
512            if json_thread.get('triggered', False):
513                self.crashlog.crashed_thread_idx = idx
514                thread.crashed = True
515                if 'threadState' in json_thread:
516                    thread.registers = self.parse_thread_registers(
517                        json_thread['threadState'])
518            thread.queue = json_thread.get('queue')
519            self.parse_frames(thread, json_thread.get('frames', []))
520            self.crashlog.threads.append(thread)
521            idx += 1
522
523    def parse_thread_registers(self, json_thread_state, prefix=None):
524        registers = dict()
525        for key, state in json_thread_state.items():
526            if key == "rosetta":
527                registers.update(self.parse_thread_registers(state))
528                continue
529            if key == "x":
530                gpr_dict = { str(idx) : reg for idx,reg in enumerate(state) }
531                registers.update(self.parse_thread_registers(gpr_dict, key))
532                continue
533            try:
534                value = int(state['value'])
535                registers["{}{}".format(prefix,key)] = value
536            except (KeyError, ValueError, TypeError):
537                pass
538        return registers
539
540    def parse_errors(self, json_data):
541       if 'reportNotes' in json_data:
542          self.crashlog.errors = json_data['reportNotes']
543
544
545class CrashLogParseMode:
546    NORMAL = 0
547    THREAD = 1
548    IMAGES = 2
549    THREGS = 3
550    SYSTEM = 4
551    INSTRS = 5
552
553
554class TextCrashLogParser:
555    parent_process_regex = re.compile('^Parent Process:\s*(.*)\[(\d+)\]')
556    thread_state_regex = re.compile('^Thread ([0-9]+) crashed with')
557    thread_instrs_regex = re.compile('^Thread ([0-9]+) instruction stream')
558    thread_regex = re.compile('^Thread ([0-9]+)([^:]*):(.*)')
559    app_backtrace_regex = re.compile('^Application Specific Backtrace ([0-9]+)([^:]*):(.*)')
560    version = r'(\(.+\)|(arm|x86_)[0-9a-z]+)\s+'
561    frame_regex = re.compile(r'^([0-9]+)' r'\s'                # id
562                             r'+(.+?)'    r'\s+'               # img_name
563                             r'(' +version+ r')?'              # img_version
564                             r'(0x[0-9a-fA-F]{7}[0-9a-fA-F]+)' # addr
565                             r' +(.*)'                         # offs
566                            )
567    null_frame_regex = re.compile(r'^([0-9]+)\s+\?\?\?\s+(0{7}0+) +(.*)')
568    image_regex_uuid = re.compile(r'(0x[0-9a-fA-F]+)'            # img_lo
569                                  r'\s+' '-' r'\s+'              #   -
570                                  r'(0x[0-9a-fA-F]+)'     r'\s+' # img_hi
571                                  r'[+]?(.+?)'            r'\s+' # img_name
572                                  r'(' +version+ ')?'            # img_version
573                                  r'(<([-0-9a-fA-F]+)>\s+)?'     # img_uuid
574                                  r'(/.*)'                       # img_path
575                                 )
576
577
578    def __init__(self, debugger, path, verbose):
579        self.path = os.path.expanduser(path)
580        self.verbose = verbose
581        self.thread = None
582        self.app_specific_backtrace = False
583        self.crashlog = CrashLog(debugger, self.path, self.verbose)
584        self.parse_mode = CrashLogParseMode.NORMAL
585        self.parsers = {
586            CrashLogParseMode.NORMAL : self.parse_normal,
587            CrashLogParseMode.THREAD : self.parse_thread,
588            CrashLogParseMode.IMAGES : self.parse_images,
589            CrashLogParseMode.THREGS : self.parse_thread_registers,
590            CrashLogParseMode.SYSTEM : self.parse_system,
591            CrashLogParseMode.INSTRS : self.parse_instructions,
592        }
593
594    def parse(self):
595        with open(self.path,'r') as f:
596            lines = f.read().splitlines()
597
598        for line in lines:
599            line_len = len(line)
600            if line_len == 0:
601                if self.thread:
602                    if self.parse_mode == CrashLogParseMode.THREAD:
603                        if self.thread.index == self.crashlog.crashed_thread_idx:
604                            self.thread.reason = ''
605                            if self.crashlog.thread_exception:
606                                self.thread.reason += self.crashlog.thread_exception
607                            if self.crashlog.thread_exception_data:
608                                self.thread.reason += " (%s)" % self.crashlog.thread_exception_data
609                        if self.app_specific_backtrace:
610                            self.crashlog.backtraces.append(self.thread)
611                        else:
612                            self.crashlog.threads.append(self.thread)
613                    self.thread = None
614                else:
615                    # only append an extra empty line if the previous line
616                    # in the info_lines wasn't empty
617                    if len(self.crashlog.info_lines) > 0 and len(self.crashlog.info_lines[-1]):
618                        self.crashlog.info_lines.append(line)
619                self.parse_mode = CrashLogParseMode.NORMAL
620            else:
621                self.parsers[self.parse_mode](line)
622
623        return self.crashlog
624
625
626    def parse_normal(self, line):
627        if line.startswith('Process:'):
628            (self.crashlog.process_name, pid_with_brackets) = line[
629                8:].strip().split(' [')
630            self.crashlog.process_id = pid_with_brackets.strip('[]')
631        elif line.startswith('Path:'):
632            self.crashlog.process_path = line[5:].strip()
633        elif line.startswith('Identifier:'):
634            self.crashlog.process_identifier = line[11:].strip()
635        elif line.startswith('Version:'):
636            version_string = line[8:].strip()
637            matched_pair = re.search("(.+)\((.+)\)", version_string)
638            if matched_pair:
639                self.crashlog.process_version = matched_pair.group(1)
640                self.crashlog.process_compatability_version = matched_pair.group(
641                    2)
642            else:
643                self.crashlog.process = version_string
644                self.crashlog.process_compatability_version = version_string
645        elif self.parent_process_regex.search(line):
646            parent_process_match = self.parent_process_regex.search(
647                line)
648            self.crashlog.parent_process_name = parent_process_match.group(1)
649            self.crashlog.parent_process_id = parent_process_match.group(2)
650        elif line.startswith('Exception Type:'):
651            self.crashlog.thread_exception = line[15:].strip()
652            return
653        elif line.startswith('Exception Codes:'):
654            self.crashlog.thread_exception_data = line[16:].strip()
655            return
656        elif line.startswith('Exception Subtype:'): # iOS
657            self.crashlog.thread_exception_data = line[18:].strip()
658            return
659        elif line.startswith('Crashed Thread:'):
660            self.crashlog.crashed_thread_idx = int(line[15:].strip().split()[0])
661            return
662        elif line.startswith('Triggered by Thread:'): # iOS
663            self.crashlog.crashed_thread_idx = int(line[20:].strip().split()[0])
664            return
665        elif line.startswith('Report Version:'):
666            self.crashlog.version = int(line[15:].strip())
667            return
668        elif line.startswith('System Profile:'):
669            self.parse_mode = CrashLogParseMode.SYSTEM
670            return
671        elif (line.startswith('Interval Since Last Report:') or
672                line.startswith('Crashes Since Last Report:') or
673                line.startswith('Per-App Interval Since Last Report:') or
674                line.startswith('Per-App Crashes Since Last Report:') or
675                line.startswith('Sleep/Wake UUID:') or
676                line.startswith('Anonymous UUID:')):
677            # ignore these
678            return
679        elif line.startswith('Thread'):
680            thread_state_match = self.thread_state_regex.search(line)
681            if thread_state_match:
682                self.app_specific_backtrace = False
683                thread_state_match = self.thread_regex.search(line)
684                thread_idx = int(thread_state_match.group(1))
685                self.parse_mode = CrashLogParseMode.THREGS
686                self.thread = self.crashlog.threads[thread_idx]
687                return
688            thread_insts_match  = self.thread_instrs_regex.search(line)
689            if thread_insts_match:
690                self.parse_mode = CrashLogParseMode.INSTRS
691                return
692            thread_match = self.thread_regex.search(line)
693            if thread_match:
694                self.app_specific_backtrace = False
695                self.parse_mode = CrashLogParseMode.THREAD
696                thread_idx = int(thread_match.group(1))
697                self.thread = self.crashlog.Thread(thread_idx, False)
698                return
699            return
700        elif line.startswith('Binary Images:'):
701            self.parse_mode = CrashLogParseMode.IMAGES
702            return
703        elif line.startswith('Application Specific Backtrace'):
704            app_backtrace_match = self.app_backtrace_regex.search(line)
705            if app_backtrace_match:
706                self.parse_mode = CrashLogParseMode.THREAD
707                self.app_specific_backtrace = True
708                idx = int(app_backtrace_match.group(1))
709                self.thread = self.crashlog.Thread(idx, True)
710        elif line.startswith('Last Exception Backtrace:'): # iOS
711            self.parse_mode = CrashLogParseMode.THREAD
712            self.app_specific_backtrace = True
713            idx = 1
714            self.thread = self.crashlog.Thread(idx, True)
715        self.crashlog.info_lines.append(line.strip())
716
717    def parse_thread(self, line):
718        if line.startswith('Thread'):
719            return
720        if self.null_frame_regex.search(line):
721            print('warning: thread parser ignored null-frame: "%s"' % line)
722            return
723        frame_match = self.frame_regex.search(line)
724        if frame_match:
725            (frame_id, frame_img_name, _, frame_img_version, _,
726                frame_addr, frame_ofs) = frame_match.groups()
727            ident = frame_img_name
728            self.thread.add_ident(ident)
729            if ident not in self.crashlog.idents:
730                self.crashlog.idents.append(ident)
731            self.thread.frames.append(self.crashlog.Frame(int(frame_id), int(
732                frame_addr, 0), frame_ofs))
733        else:
734            print('error: frame regex failed for line: "%s"' % line)
735
736    def parse_images(self, line):
737        image_match = self.image_regex_uuid.search(line)
738        if image_match:
739            (img_lo, img_hi, img_name, _, img_version, _,
740                _, img_uuid, img_path) = image_match.groups()
741            image = self.crashlog.DarwinImage(int(img_lo, 0), int(img_hi, 0),
742                                            img_name.strip(),
743                                            img_version.strip()
744                                            if img_version else "",
745                                            uuid.UUID(img_uuid), img_path,
746                                            self.verbose)
747            self.crashlog.images.append(image)
748        else:
749            print("error: image regex failed for: %s" % line)
750
751
752    def parse_thread_registers(self, line):
753        stripped_line = line.strip()
754        # "r12: 0x00007fff6b5939c8  r13: 0x0000000007000006  r14: 0x0000000000002a03  r15: 0x0000000000000c00"
755        reg_values = re.findall(
756            '([a-zA-Z0-9]+: 0[Xx][0-9a-fA-F]+) *', stripped_line)
757        for reg_value in reg_values:
758            (reg, value) = reg_value.split(': ')
759            self.thread.registers[reg.strip()] = int(value, 0)
760
761    def parse_system(self, line):
762        self.crashlog.system_profile.append(line)
763
764    def parse_instructions(self, line):
765        pass
766
767
768def usage():
769    print("Usage: lldb-symbolicate.py [-n name] executable-image")
770    sys.exit(0)
771
772
773class Interactive(cmd.Cmd):
774    '''Interactive prompt for analyzing one or more Darwin crash logs, type "help" to see a list of supported commands.'''
775    image_option_parser = None
776
777    def __init__(self, crash_logs):
778        cmd.Cmd.__init__(self)
779        self.use_rawinput = False
780        self.intro = 'Interactive crashlogs prompt, type "help" to see a list of supported commands.'
781        self.crash_logs = crash_logs
782        self.prompt = '% '
783
784    def default(self, line):
785        '''Catch all for unknown command, which will exit the interpreter.'''
786        print("uknown command: %s" % line)
787        return True
788
789    def do_q(self, line):
790        '''Quit command'''
791        return True
792
793    def do_quit(self, line):
794        '''Quit command'''
795        return True
796
797    def do_symbolicate(self, line):
798        description = '''Symbolicate one or more darwin crash log files by index to provide source file and line information,
799        inlined stack frames back to the concrete functions, and disassemble the location of the crash
800        for the first frame of the crashed thread.'''
801        option_parser = CreateSymbolicateCrashLogOptions(
802            'symbolicate', description, False)
803        command_args = shlex.split(line)
804        try:
805            (options, args) = option_parser.parse_args(command_args)
806        except:
807            return
808
809        if args:
810            # We have arguments, they must valid be crash log file indexes
811            for idx_str in args:
812                idx = int(idx_str)
813                if idx < len(self.crash_logs):
814                    SymbolicateCrashLog(self.crash_logs[idx], options)
815                else:
816                    print('error: crash log index %u is out of range' % (idx))
817        else:
818            # No arguments, symbolicate all crash logs using the options
819            # provided
820            for idx in range(len(self.crash_logs)):
821                SymbolicateCrashLog(self.crash_logs[idx], options)
822
823    def do_list(self, line=None):
824        '''Dump a list of all crash logs that are currently loaded.
825
826        USAGE: list'''
827        print('%u crash logs are loaded:' % len(self.crash_logs))
828        for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
829            print('[%u] = %s' % (crash_log_idx, crash_log.path))
830
831    def do_image(self, line):
832        '''Dump information about one or more binary images in the crash log given an image basename, or all images if no arguments are provided.'''
833        usage = "usage: %prog [options] <PATH> [PATH ...]"
834        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.'''
835        command_args = shlex.split(line)
836        if not self.image_option_parser:
837            self.image_option_parser = optparse.OptionParser(
838                description=description, prog='image', usage=usage)
839            self.image_option_parser.add_option(
840                '-a',
841                '--all',
842                action='store_true',
843                help='show all images',
844                default=False)
845        try:
846            (options, args) = self.image_option_parser.parse_args(command_args)
847        except:
848            return
849
850        if args:
851            for image_path in args:
852                fullpath_search = image_path[0] == '/'
853                for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
854                    matches_found = 0
855                    for (image_idx, image) in enumerate(crash_log.images):
856                        if fullpath_search:
857                            if image.get_resolved_path() == image_path:
858                                matches_found += 1
859                                print('[%u] ' % (crash_log_idx), image)
860                        else:
861                            image_basename = image.get_resolved_path_basename()
862                            if image_basename == image_path:
863                                matches_found += 1
864                                print('[%u] ' % (crash_log_idx), image)
865                    if matches_found == 0:
866                        for (image_idx, image) in enumerate(crash_log.images):
867                            resolved_image_path = image.get_resolved_path()
868                            if resolved_image_path and string.find(
869                                    image.get_resolved_path(), image_path) >= 0:
870                                print('[%u] ' % (crash_log_idx), image)
871        else:
872            for crash_log in self.crash_logs:
873                for (image_idx, image) in enumerate(crash_log.images):
874                    print('[%u] %s' % (image_idx, image))
875        return False
876
877
878def interactive_crashlogs(debugger, options, args):
879    crash_log_files = list()
880    for arg in args:
881        for resolved_path in glob.glob(arg):
882            crash_log_files.append(resolved_path)
883
884    crash_logs = list()
885    for crash_log_file in crash_log_files:
886        try:
887            crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose)
888        except Exception as e:
889            print(e)
890            continue
891        if options.debug:
892            crash_log.dump()
893        if not crash_log.images:
894            print('error: no images in crash log "%s"' % (crash_log))
895            continue
896        else:
897            crash_logs.append(crash_log)
898
899    interpreter = Interactive(crash_logs)
900    # List all crash logs that were imported
901    interpreter.do_list()
902    interpreter.cmdloop()
903
904
905def save_crashlog(debugger, command, exe_ctx, result, dict):
906    usage = "usage: %prog [options] <output-path>"
907    description = '''Export the state of current target into a crashlog file'''
908    parser = optparse.OptionParser(
909        description=description,
910        prog='save_crashlog',
911        usage=usage)
912    parser.add_option(
913        '-v',
914        '--verbose',
915        action='store_true',
916        dest='verbose',
917        help='display verbose debug info',
918        default=False)
919    try:
920        (options, args) = parser.parse_args(shlex.split(command))
921    except:
922        result.PutCString("error: invalid options")
923        return
924    if len(args) != 1:
925        result.PutCString(
926            "error: invalid arguments, a single output file is the only valid argument")
927        return
928    out_file = open(args[0], 'w')
929    if not out_file:
930        result.PutCString(
931            "error: failed to open file '%s' for writing...",
932            args[0])
933        return
934    target = exe_ctx.target
935    if target:
936        identifier = target.executable.basename
937        process = exe_ctx.process
938        if process:
939            pid = process.id
940            if pid != lldb.LLDB_INVALID_PROCESS_ID:
941                out_file.write(
942                    'Process:         %s [%u]\n' %
943                    (identifier, pid))
944        out_file.write('Path:            %s\n' % (target.executable.fullpath))
945        out_file.write('Identifier:      %s\n' % (identifier))
946        out_file.write('\nDate/Time:       %s\n' %
947                       (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
948        out_file.write(
949            'OS Version:      Mac OS X %s (%s)\n' %
950            (platform.mac_ver()[0], subprocess.check_output('sysctl -n kern.osversion', shell=True).decode("utf-8")))
951        out_file.write('Report Version:  9\n')
952        for thread_idx in range(process.num_threads):
953            thread = process.thread[thread_idx]
954            out_file.write('\nThread %u:\n' % (thread_idx))
955            for (frame_idx, frame) in enumerate(thread.frames):
956                frame_pc = frame.pc
957                frame_offset = 0
958                if frame.function:
959                    block = frame.GetFrameBlock()
960                    block_range = block.range[frame.addr]
961                    if block_range:
962                        block_start_addr = block_range[0]
963                        frame_offset = frame_pc - block_start_addr.GetLoadAddress(target)
964                    else:
965                        frame_offset = frame_pc - frame.function.addr.GetLoadAddress(target)
966                elif frame.symbol:
967                    frame_offset = frame_pc - frame.symbol.addr.GetLoadAddress(target)
968                out_file.write(
969                    '%-3u %-32s 0x%16.16x %s' %
970                    (frame_idx, frame.module.file.basename, frame_pc, frame.name))
971                if frame_offset > 0:
972                    out_file.write(' + %u' % (frame_offset))
973                line_entry = frame.line_entry
974                if line_entry:
975                    if options.verbose:
976                        # This will output the fullpath + line + column
977                        out_file.write(' %s' % (line_entry))
978                    else:
979                        out_file.write(
980                            ' %s:%u' %
981                            (line_entry.file.basename, line_entry.line))
982                        column = line_entry.column
983                        if column:
984                            out_file.write(':%u' % (column))
985                out_file.write('\n')
986
987        out_file.write('\nBinary Images:\n')
988        for module in target.modules:
989            text_segment = module.section['__TEXT']
990            if text_segment:
991                text_segment_load_addr = text_segment.GetLoadAddress(target)
992                if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS:
993                    text_segment_end_load_addr = text_segment_load_addr + text_segment.size
994                    identifier = module.file.basename
995                    module_version = '???'
996                    module_version_array = module.GetVersion()
997                    if module_version_array:
998                        module_version = '.'.join(
999                            map(str, module_version_array))
1000                    out_file.write(
1001                        '    0x%16.16x - 0x%16.16x  %s (%s - ???) <%s> %s\n' %
1002                        (text_segment_load_addr,
1003                         text_segment_end_load_addr,
1004                         identifier,
1005                         module_version,
1006                         module.GetUUIDString(),
1007                         module.file.fullpath))
1008        out_file.close()
1009    else:
1010        result.PutCString("error: invalid target")
1011
1012
1013class Symbolicate:
1014    def __init__(self, debugger, internal_dict):
1015        pass
1016
1017    def __call__(self, debugger, command, exe_ctx, result):
1018        try:
1019            SymbolicateCrashLogs(debugger, shlex.split(command))
1020        except Exception as e:
1021            result.PutCString("error: python exception: %s" % e)
1022
1023    def get_short_help(self):
1024        return "Symbolicate one or more darwin crash log files."
1025
1026    def get_long_help(self):
1027        option_parser = CrashLogOptionParser()
1028        return option_parser.format_help()
1029
1030
1031def SymbolicateCrashLog(crash_log, options):
1032    if options.debug:
1033        crash_log.dump()
1034    if not crash_log.images:
1035        print('error: no images in crash log')
1036        return
1037
1038    if options.dump_image_list:
1039        print("Binary Images:")
1040        for image in crash_log.images:
1041            if options.verbose:
1042                print(image.debug_dump())
1043            else:
1044                print(image)
1045
1046    target = crash_log.create_target()
1047    if not target:
1048        return
1049    exe_module = target.GetModuleAtIndex(0)
1050    images_to_load = list()
1051    loaded_images = list()
1052    if options.load_all_images:
1053        # --load-all option was specified, load everything up
1054        for image in crash_log.images:
1055            images_to_load.append(image)
1056    else:
1057        # Only load the images found in stack frames for the crashed threads
1058        if options.crashed_only:
1059            for thread in crash_log.threads:
1060                if thread.did_crash():
1061                    for ident in thread.idents:
1062                        images = crash_log.find_images_with_identifier(ident)
1063                        if images:
1064                            for image in images:
1065                                images_to_load.append(image)
1066                        else:
1067                            print('error: can\'t find image for identifier "%s"' % ident)
1068        else:
1069            for ident in crash_log.idents:
1070                images = crash_log.find_images_with_identifier(ident)
1071                if images:
1072                    for image in images:
1073                        images_to_load.append(image)
1074                else:
1075                    print('error: can\'t find image for identifier "%s"' % ident)
1076
1077    for image in images_to_load:
1078        if image not in loaded_images:
1079            err = image.add_module(target)
1080            if err:
1081                print(err)
1082            else:
1083                loaded_images.append(image)
1084
1085    if crash_log.backtraces:
1086        for thread in crash_log.backtraces:
1087            thread.dump_symbolicated(crash_log, options)
1088            print()
1089
1090    for thread in crash_log.threads:
1091        thread.dump_symbolicated(crash_log, options)
1092        print()
1093
1094    if crash_log.errors:
1095        print("Errors:")
1096        for error in crash_log.errors:
1097            print(error)
1098
1099
1100def CreateSymbolicateCrashLogOptions(
1101        command_name,
1102        description,
1103        add_interactive_options):
1104    usage = "usage: %prog [options] <FILE> [FILE ...]"
1105    option_parser = optparse.OptionParser(
1106        description=description, prog='crashlog', usage=usage)
1107    option_parser.add_option(
1108        '--verbose',
1109        '-v',
1110        action='store_true',
1111        dest='verbose',
1112        help='display verbose debug info',
1113        default=False)
1114    option_parser.add_option(
1115        '--debug',
1116        '-g',
1117        action='store_true',
1118        dest='debug',
1119        help='display verbose debug logging',
1120        default=False)
1121    option_parser.add_option(
1122        '--load-all',
1123        '-a',
1124        action='store_true',
1125        dest='load_all_images',
1126        help='load all executable images, not just the images found in the crashed stack frames',
1127        default=False)
1128    option_parser.add_option(
1129        '--images',
1130        action='store_true',
1131        dest='dump_image_list',
1132        help='show image list',
1133        default=False)
1134    option_parser.add_option(
1135        '--debug-delay',
1136        type='int',
1137        dest='debug_delay',
1138        metavar='NSEC',
1139        help='pause for NSEC seconds for debugger',
1140        default=0)
1141    option_parser.add_option(
1142        '--crashed-only',
1143        '-c',
1144        action='store_true',
1145        dest='crashed_only',
1146        help='only symbolicate the crashed thread',
1147        default=False)
1148    option_parser.add_option(
1149        '--disasm-depth',
1150        '-d',
1151        type='int',
1152        dest='disassemble_depth',
1153        help='set the depth in stack frames that should be disassembled (default is 1)',
1154        default=1)
1155    option_parser.add_option(
1156        '--disasm-all',
1157        '-D',
1158        action='store_true',
1159        dest='disassemble_all_threads',
1160        help='enabled disassembly of frames on all threads (not just the crashed thread)',
1161        default=False)
1162    option_parser.add_option(
1163        '--disasm-before',
1164        '-B',
1165        type='int',
1166        dest='disassemble_before',
1167        help='the number of instructions to disassemble before the frame PC',
1168        default=4)
1169    option_parser.add_option(
1170        '--disasm-after',
1171        '-A',
1172        type='int',
1173        dest='disassemble_after',
1174        help='the number of instructions to disassemble after the frame PC',
1175        default=4)
1176    option_parser.add_option(
1177        '--source-context',
1178        '-C',
1179        type='int',
1180        metavar='NLINES',
1181        dest='source_context',
1182        help='show NLINES source lines of source context (default = 4)',
1183        default=4)
1184    option_parser.add_option(
1185        '--source-frames',
1186        type='int',
1187        metavar='NFRAMES',
1188        dest='source_frames',
1189        help='show source for NFRAMES (default = 4)',
1190        default=4)
1191    option_parser.add_option(
1192        '--source-all',
1193        action='store_true',
1194        dest='source_all',
1195        help='show source for all threads, not just the crashed thread',
1196        default=False)
1197    if add_interactive_options:
1198        option_parser.add_option(
1199            '-i',
1200            '--interactive',
1201            action='store_true',
1202            help='parse all crash logs and enter interactive mode',
1203            default=False)
1204    return option_parser
1205
1206
1207def CrashLogOptionParser():
1208    description = '''Symbolicate one or more darwin crash log files to provide source file and line information,
1209inlined stack frames back to the concrete functions, and disassemble the location of the crash
1210for the first frame of the crashed thread.
1211If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter
1212for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been
1213created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows
1214you to explore the program as if it were stopped at the locations described in the crash log and functions can
1215be disassembled and lookups can be performed using the addresses found in the crash log.'''
1216    return CreateSymbolicateCrashLogOptions('crashlog', description, True)
1217
1218def SymbolicateCrashLogs(debugger, command_args):
1219    option_parser = CrashLogOptionParser()
1220    try:
1221        (options, args) = option_parser.parse_args(command_args)
1222    except:
1223        return
1224
1225    if options.debug:
1226        print('command_args = %s' % command_args)
1227        print('options', options)
1228        print('args', args)
1229
1230    if options.debug_delay > 0:
1231        print("Waiting %u seconds for debugger to attach..." % options.debug_delay)
1232        time.sleep(options.debug_delay)
1233    error = lldb.SBError()
1234
1235    if args:
1236        if options.interactive:
1237            interactive_crashlogs(debugger, options, args)
1238        else:
1239            for crash_log_file in args:
1240                crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose)
1241                SymbolicateCrashLog(crash_log, options)
1242
1243if __name__ == '__main__':
1244    # Create a new debugger instance
1245    debugger = lldb.SBDebugger.Create()
1246    SymbolicateCrashLogs(debugger, sys.argv[1:])
1247    lldb.SBDebugger.Destroy(debugger)
1248
1249def __lldb_init_module(debugger, internal_dict):
1250    debugger.HandleCommand(
1251        'command script add -c lldb.macosx.crashlog.Symbolicate crashlog')
1252    debugger.HandleCommand(
1253        'command script add -f lldb.macosx.crashlog.save_crashlog save_crashlog')
1254    print('"crashlog" and "save_crashlog" commands have been installed, use '
1255          'the "--help" options on these commands for detailed help.')
1256