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