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