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