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