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    if debugger.GetNumTargets() > 0:
993        target = debugger.GetTargetAtIndex(0)
994    else:
995        target = crashlog.create_target()
996    if not target:
997        result.PutCString("error: couldn't create target")
998        return
999
1000    ci = debugger.GetCommandInterpreter()
1001    if not ci:
1002        result.PutCString("error: couldn't get command interpreter")
1003        return
1004
1005    res = lldb.SBCommandReturnObject()
1006    ci.HandleCommand('script from lldb.macosx import crashlog_scripted_process', res)
1007    if not res.Succeeded():
1008        result.PutCString("error: couldn't import crashlog scripted process module")
1009        return
1010
1011    structured_data = lldb.SBStructuredData()
1012    structured_data.SetFromJSON(json.dumps({ "crashlog_path" : crashlog_path }))
1013    launch_info = lldb.SBLaunchInfo(None)
1014    launch_info.SetProcessPluginName("ScriptedProcess")
1015    launch_info.SetScriptedProcessClassName("crashlog_scripted_process.CrashLogScriptedProcess")
1016    launch_info.SetScriptedProcessDictionary(structured_data)
1017    error = lldb.SBError()
1018    process = target.Launch(launch_info, error)
1019
1020def CreateSymbolicateCrashLogOptions(
1021        command_name,
1022        description,
1023        add_interactive_options):
1024    usage = "usage: %prog [options] <FILE> [FILE ...]"
1025    option_parser = optparse.OptionParser(
1026        description=description, prog='crashlog', usage=usage)
1027    option_parser.add_option(
1028        '--verbose',
1029        '-v',
1030        action='store_true',
1031        dest='verbose',
1032        help='display verbose debug info',
1033        default=False)
1034    option_parser.add_option(
1035        '--debug',
1036        '-g',
1037        action='store_true',
1038        dest='debug',
1039        help='display verbose debug logging',
1040        default=False)
1041    option_parser.add_option(
1042        '--load-all',
1043        '-a',
1044        action='store_true',
1045        dest='load_all_images',
1046        help='load all executable images, not just the images found in the crashed stack frames',
1047        default=False)
1048    option_parser.add_option(
1049        '--images',
1050        action='store_true',
1051        dest='dump_image_list',
1052        help='show image list',
1053        default=False)
1054    option_parser.add_option(
1055        '--debug-delay',
1056        type='int',
1057        dest='debug_delay',
1058        metavar='NSEC',
1059        help='pause for NSEC seconds for debugger',
1060        default=0)
1061    option_parser.add_option(
1062        '--crashed-only',
1063        '-c',
1064        action='store_true',
1065        dest='crashed_only',
1066        help='only symbolicate the crashed thread',
1067        default=False)
1068    option_parser.add_option(
1069        '--disasm-depth',
1070        '-d',
1071        type='int',
1072        dest='disassemble_depth',
1073        help='set the depth in stack frames that should be disassembled (default is 1)',
1074        default=1)
1075    option_parser.add_option(
1076        '--disasm-all',
1077        '-D',
1078        action='store_true',
1079        dest='disassemble_all_threads',
1080        help='enabled disassembly of frames on all threads (not just the crashed thread)',
1081        default=False)
1082    option_parser.add_option(
1083        '--disasm-before',
1084        '-B',
1085        type='int',
1086        dest='disassemble_before',
1087        help='the number of instructions to disassemble before the frame PC',
1088        default=4)
1089    option_parser.add_option(
1090        '--disasm-after',
1091        '-A',
1092        type='int',
1093        dest='disassemble_after',
1094        help='the number of instructions to disassemble after the frame PC',
1095        default=4)
1096    option_parser.add_option(
1097        '--source-context',
1098        '-C',
1099        type='int',
1100        metavar='NLINES',
1101        dest='source_context',
1102        help='show NLINES source lines of source context (default = 4)',
1103        default=4)
1104    option_parser.add_option(
1105        '--source-frames',
1106        type='int',
1107        metavar='NFRAMES',
1108        dest='source_frames',
1109        help='show source for NFRAMES (default = 4)',
1110        default=4)
1111    option_parser.add_option(
1112        '--source-all',
1113        action='store_true',
1114        dest='source_all',
1115        help='show source for all threads, not just the crashed thread',
1116        default=False)
1117    if add_interactive_options:
1118        option_parser.add_option(
1119            '-i',
1120            '--interactive',
1121            action='store_true',
1122            help='parse a crash log and load it in a ScriptedProcess',
1123            default=False)
1124        option_parser.add_option(
1125            '-b',
1126            '--batch',
1127            action='store_true',
1128            help='dump symbolicated stackframes without creating a debug session',
1129            default=True)
1130    return option_parser
1131
1132
1133def CrashLogOptionParser():
1134    description = '''Symbolicate one or more darwin crash log files to provide source file and line information,
1135inlined stack frames back to the concrete functions, and disassemble the location of the crash
1136for the first frame of the crashed thread.
1137If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter
1138for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been
1139created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows
1140you to explore the program as if it were stopped at the locations described in the crash log and functions can
1141be disassembled and lookups can be performed using the addresses found in the crash log.'''
1142    return CreateSymbolicateCrashLogOptions('crashlog', description, True)
1143
1144def SymbolicateCrashLogs(debugger, command_args):
1145    option_parser = CrashLogOptionParser()
1146    try:
1147        (options, args) = option_parser.parse_args(command_args)
1148    except:
1149        return
1150
1151    if options.debug:
1152        print('command_args = %s' % command_args)
1153        print('options', options)
1154        print('args', args)
1155
1156    if options.debug_delay > 0:
1157        print("Waiting %u seconds for debugger to attach..." % options.debug_delay)
1158        time.sleep(options.debug_delay)
1159    error = lldb.SBError()
1160
1161    def should_run_in_interactive_mode(options, ci):
1162        if options.interactive:
1163            return True
1164        elif options.batch:
1165            return False
1166        # elif ci and ci.IsInteractive():
1167        #     return True
1168        else:
1169            return False
1170
1171    ci = debugger.GetCommandInterpreter()
1172
1173    if args:
1174        for crash_log_file in args:
1175            if should_run_in_interactive_mode(options, ci):
1176                load_crashlog_in_scripted_process(debugger, crash_log_file)
1177            else:
1178                crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose)
1179                SymbolicateCrashLog(crash_log, options)
1180
1181if __name__ == '__main__':
1182    # Create a new debugger instance
1183    debugger = lldb.SBDebugger.Create()
1184    SymbolicateCrashLogs(debugger, sys.argv[1:])
1185    lldb.SBDebugger.Destroy(debugger)
1186
1187def __lldb_init_module(debugger, internal_dict):
1188    debugger.HandleCommand(
1189        'command script add -c lldb.macosx.crashlog.Symbolicate crashlog')
1190    debugger.HandleCommand(
1191        'command script add -f lldb.macosx.crashlog.save_crashlog save_crashlog')
1192    print('"crashlog" and "save_crashlog" commands have been installed, use '
1193          'the "--help" options on these commands for detailed help.')
1194