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