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