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