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