1#!/usr/bin/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
29import lldb
30import commands
31import cmd
32import glob
33import optparse
34import os
35import plistlib
36import pprint # pp = pprint.PrettyPrinter(indent=4); pp.pprint(command_args)
37import re
38import shlex
39import string
40import sys
41import time
42import uuid
43from lldb.utils import symbolication
44
45PARSE_MODE_NORMAL = 0
46PARSE_MODE_THREAD = 1
47PARSE_MODE_IMAGES = 2
48PARSE_MODE_THREGS = 3
49PARSE_MODE_SYSTEM = 4
50
51class CrashLog(symbolication.Symbolicator):
52    """Class that does parses darwin crash logs"""
53    thread_state_regex = re.compile('^Thread ([0-9]+) crashed with')
54    thread_regex = re.compile('^Thread ([0-9]+)([^:]*):(.*)')
55    frame_regex = re.compile('^([0-9]+) +([^ ]+) *\t(0x[0-9a-fA-F]+) +(.*)')
56    image_regex_uuid = re.compile('(0x[0-9a-fA-F]+)[- ]+(0x[0-9a-fA-F]+) +[+]?([^ ]+) +([^<]+)<([-0-9a-fA-F]+)> (.*)');
57    image_regex_no_uuid = re.compile('(0x[0-9a-fA-F]+)[- ]+(0x[0-9a-fA-F]+) +[+]?([^ ]+) +([^/]+)/(.*)');
58    empty_line_regex = re.compile('^$')
59
60    class Thread:
61        """Class that represents a thread in a darwin crash log"""
62        def __init__(self, index):
63            self.index = index
64            self.frames = list()
65            self.registers = dict()
66            self.reason = None
67            self.queue = None
68
69        def dump(self, prefix):
70            print "%sThread[%u] %s" % (prefix, self.index, self.reason)
71            if self.frames:
72                print "%s  Frames:" % (prefix)
73                for frame in self.frames:
74                    frame.dump(prefix + '    ')
75            if self.registers:
76                print "%s  Registers:" % (prefix)
77                for reg in self.registers.keys():
78                    print "%s    %-5s = %#16.16x" % (prefix, reg, self.registers[reg])
79
80        def did_crash(self):
81            return self.reason != None
82
83        def __str__(self):
84            s = "Thread[%u]" % self.index
85            if self.reason:
86                s += ' %s' % self.reason
87            return s
88
89
90    class Frame:
91        """Class that represents a stack frame in a thread in a darwin crash log"""
92        def __init__(self, index, pc, description):
93            self.pc = pc
94            self.description = description
95            self.index = index
96
97        def __str__(self):
98            if self.description:
99                return "[%3u] 0x%16.16x %s" % (self.index, self.pc, self.description)
100            else:
101                return "[%3u] 0x%16.16x" % (self.index, self.pc)
102
103        def dump(self, prefix):
104            print "%s%s" % (prefix, str(self))
105
106    class DarwinImage(symbolication.Image):
107        """Class that represents a binary images in a darwin crash log"""
108        dsymForUUIDBinary = os.path.expanduser('~rc/bin/dsymForUUID')
109        if not os.path.exists(dsymForUUIDBinary):
110            dsymForUUIDBinary = commands.getoutput('which dsymForUUID')
111
112        dwarfdump_uuid_regex = re.compile('UUID: ([-0-9a-fA-F]+) \(([^\(]+)\) .*')
113
114        def __init__(self, text_addr_lo, text_addr_hi, identifier, version, uuid, path):
115            symbolication.Image.__init__(self, path, uuid);
116            self.add_section (symbolication.Section(text_addr_lo, text_addr_hi, "__TEXT"))
117            self.identifier = identifier
118            self.version = version
119
120        def locate_module_and_debug_symbols(self):
121            if self.resolved_path:
122                # Don't load a module twice...
123                return True
124            uuid_str = self.get_normalized_uuid_string()
125            print 'Getting symbols for %s %s...' % (uuid_str, self.path),
126            if os.path.exists(self.dsymForUUIDBinary):
127                dsym_for_uuid_command = '%s %s' % (self.dsymForUUIDBinary, uuid_str)
128                s = commands.getoutput(dsym_for_uuid_command)
129                if s:
130                    plist_root = plistlib.readPlistFromString (s)
131                    if plist_root:
132                        plist = plist_root[uuid_str]
133                        if plist:
134                            if 'DBGArchitecture' in plist:
135                                self.arch = plist['DBGArchitecture']
136                            if 'DBGDSYMPath' in plist:
137                                self.symfile = os.path.realpath(plist['DBGDSYMPath'])
138                            if 'DBGSymbolRichExecutable' in plist:
139                                self.resolved_path = os.path.expanduser (plist['DBGSymbolRichExecutable'])
140            if not self.resolved_path and os.path.exists(self.path):
141                dwarfdump_cmd_output = commands.getoutput('dwarfdump --uuid "%s"' % self.path)
142                self_uuid = self.get_uuid()
143                for line in dwarfdump_cmd_output.splitlines():
144                    match = self.dwarfdump_uuid_regex.search (line)
145                    if match:
146                        dwarf_uuid_str = match.group(1)
147                        dwarf_uuid = uuid.UUID(dwarf_uuid_str)
148                        if self_uuid == dwarf_uuid:
149                            self.resolved_path = self.path
150                            self.arch = match.group(2)
151                            break;
152                if not self.resolved_path:
153                    print "error: file %s '%s' doesn't match the UUID in the installed file" % (uuid_str, self.path)
154                    return False
155            if (self.resolved_path and os.path.exists(self.resolved_path)) or (self.path and os.path.exists(self.path)):
156                print 'ok'
157                # if self.resolved_path:
158                #     print '  exe = "%s"' % self.resolved_path
159                # if self.symfile:
160                #     print ' dsym = "%s"' % self.symfile
161                return True
162            return False
163
164
165
166    def __init__(self, path):
167        """CrashLog constructor that take a path to a darwin crash log file"""
168        symbolication.Symbolicator.__init__(self);
169        self.path = os.path.expanduser(path);
170        self.info_lines = list()
171        self.system_profile = list()
172        self.threads = list()
173        self.idents = list() # A list of the required identifiers for doing all stack backtraces
174        self.crashed_thread_idx = -1
175        self.version = -1
176        self.error = None
177        # With possible initial component of ~ or ~user replaced by that user's home directory.
178        try:
179            f = open(self.path)
180        except IOError:
181            self.error = 'error: cannot open "%s"' % self.path
182            return
183
184        self.file_lines = f.read().splitlines()
185        parse_mode = PARSE_MODE_NORMAL
186        thread = None
187        for line in self.file_lines:
188            # print line
189            line_len = len(line)
190            if line_len == 0:
191                if thread:
192                    if parse_mode == PARSE_MODE_THREAD:
193                        if thread.index == self.crashed_thread_idx:
194                            thread.reason = ''
195                            if self.thread_exception:
196                                thread.reason += self.thread_exception
197                            if self.thread_exception_data:
198                                thread.reason += " (%s)" % self.thread_exception_data
199                        self.threads.append(thread)
200                    thread = None
201                else:
202                    # only append an extra empty line if the previous line
203                    # in the info_lines wasn't empty
204                    if len(self.info_lines) > 0 and len(self.info_lines[-1]):
205                        self.info_lines.append(line)
206                parse_mode = PARSE_MODE_NORMAL
207                # print 'PARSE_MODE_NORMAL'
208            elif parse_mode == PARSE_MODE_NORMAL:
209                if line.startswith ('Process:'):
210                    (self.process_name, pid_with_brackets) = line[8:].strip().split()
211                    self.process_id = pid_with_brackets.strip('[]')
212                elif line.startswith ('Path:'):
213                    self.process_path = line[5:].strip()
214                elif line.startswith ('Identifier:'):
215                    self.process_identifier = line[11:].strip()
216                elif line.startswith ('Version:'):
217                    version_string = line[8:].strip()
218                    matched_pair = re.search("(.+)\((.+)\)", version_string)
219                    if matched_pair:
220                        self.process_version = matched_pair.group(1)
221                        self.process_compatability_version = matched_pair.group(2)
222                    else:
223                        self.process = version_string
224                        self.process_compatability_version = version_string
225                elif line.startswith ('Parent Process:'):
226                    (self.parent_process_name, pid_with_brackets) = line[15:].strip().split()
227                    self.parent_process_id = pid_with_brackets.strip('[]')
228                elif line.startswith ('Exception Type:'):
229                    self.thread_exception = line[15:].strip()
230                    continue
231                elif line.startswith ('Exception Codes:'):
232                    self.thread_exception_data = line[16:].strip()
233                    continue
234                elif line.startswith ('Crashed Thread:'):
235                    self.crashed_thread_idx = int(line[15:].strip().split()[0])
236                    continue
237                elif line.startswith ('Report Version:'):
238                    self.version = int(line[15:].strip())
239                    continue
240                elif line.startswith ('System Profile:'):
241                    parse_mode = PARSE_MODE_SYSTEM
242                    continue
243                elif (line.startswith ('Interval Since Last Report:') or
244                      line.startswith ('Crashes Since Last Report:') or
245                      line.startswith ('Per-App Interval Since Last Report:') or
246                      line.startswith ('Per-App Crashes Since Last Report:') or
247                      line.startswith ('Sleep/Wake UUID:') or
248                      line.startswith ('Anonymous UUID:')):
249                    # ignore these
250                    continue
251                elif line.startswith ('Thread'):
252                    thread_state_match = self.thread_state_regex.search (line)
253                    if thread_state_match:
254                        thread_state_match = self.thread_regex.search (line)
255                        thread_idx = int(thread_state_match.group(1))
256                        parse_mode = PARSE_MODE_THREGS
257                        thread = self.threads[thread_idx]
258                    else:
259                        thread_match = self.thread_regex.search (line)
260                        if thread_match:
261                            # print 'PARSE_MODE_THREAD'
262                            parse_mode = PARSE_MODE_THREAD
263                            thread_idx = int(thread_match.group(1))
264                            thread = CrashLog.Thread(thread_idx)
265                    continue
266                elif line.startswith ('Binary Images:'):
267                    parse_mode = PARSE_MODE_IMAGES
268                    continue
269                self.info_lines.append(line.strip())
270            elif parse_mode == PARSE_MODE_THREAD:
271                if line.startswith ('Thread'):
272                    continue
273                frame_match = self.frame_regex.search(line)
274                if frame_match:
275                    ident = frame_match.group(2)
276                    if not ident in self.idents:
277                        self.idents.append(ident)
278                    thread.frames.append (CrashLog.Frame(int(frame_match.group(1)), int(frame_match.group(3), 0), frame_match.group(4)))
279                else:
280                    print 'error: frame regex failed for line: "%s"' % line
281            elif parse_mode == PARSE_MODE_IMAGES:
282                image_match = self.image_regex_uuid.search (line)
283                if image_match:
284                    image = CrashLog.DarwinImage (int(image_match.group(1),0),
285                                                  int(image_match.group(2),0),
286                                                  image_match.group(3).strip(),
287                                                  image_match.group(4).strip(),
288                                                  uuid.UUID(image_match.group(5)),
289                                                  image_match.group(6))
290                    self.images.append (image)
291                else:
292                    image_match = self.image_regex_no_uuid.search (line)
293                    if image_match:
294                        image = CrashLog.DarwinImage (int(image_match.group(1),0),
295                                                      int(image_match.group(2),0),
296                                                      image_match.group(3).strip(),
297                                                      image_match.group(4).strip(),
298                                                      None,
299                                                      image_match.group(5))
300                        self.images.append (image)
301                    else:
302                        print "error: image regex failed for: %s" % line
303
304            elif parse_mode == PARSE_MODE_THREGS:
305                stripped_line = line.strip()
306                reg_values = re.split('  +', stripped_line);
307                for reg_value in reg_values:
308                    #print 'reg_value = "%s"' % reg_value
309                    (reg, value) = reg_value.split(': ')
310                    #print 'reg = "%s"' % reg
311                    #print 'value = "%s"' % value
312                    thread.registers[reg.strip()] = int(value, 0)
313            elif parse_mode == PARSE_MODE_SYSTEM:
314                self.system_profile.append(line)
315        f.close()
316
317    def dump(self):
318        print "Crash Log File: %s" % (self.path)
319        print "\nThreads:"
320        for thread in self.threads:
321            thread.dump('  ')
322        print "\nImages:"
323        for image in self.images:
324            image.dump('  ')
325
326    def find_image_with_identifier(self, identifier):
327        for image in self.images:
328            if image.identifier == identifier:
329                return image
330        return None
331
332    def create_target(self):
333        #print 'crashlog.create_target()...'
334        target = symbolication.Symbolicator.create_target(self)
335        if target:
336            return target
337        # We weren't able to open the main executable as, but we can still symbolicate
338        print 'crashlog.create_target()...2'
339        if self.idents:
340            for ident in self.idents:
341                image = self.find_image_with_identifier (ident)
342                if image:
343                    target = image.create_target ()
344                    if target:
345                        return target # success
346        print 'crashlog.create_target()...3'
347        for image in self.images:
348            target = image.create_target ()
349            if target:
350                return target # success
351        print 'crashlog.create_target()...4'
352        print 'error: unable to locate any executables from the crash log'
353        return None
354
355
356def usage():
357    print "Usage: lldb-symbolicate.py [-n name] executable-image"
358    sys.exit(0)
359
360class Interactive(cmd.Cmd):
361    '''Interactive prompt for analyzing one or more Darwin crash logs, type "help" to see a list of supported commands.'''
362    image_option_parser = None
363
364    def __init__(self, crash_logs):
365        cmd.Cmd.__init__(self)
366        self.intro = 'Interactive crashlogs prompt, type "help" to see a list of supported commands.'
367        self.crash_logs = crash_logs
368        self.prompt = '% '
369
370    def default(self, line):
371        '''Catch all for unknown command, which will exit the interpreter.'''
372        print "uknown command: %s" % line
373        return True
374
375    def do_q(self, line):
376        '''Quit command'''
377        return True
378
379    def do_quit(self, line):
380        '''Quit command'''
381        return True
382
383    def do_list(self, line=None):
384        '''Dump a list of all crash logs that are currently loaded.
385
386        USAGE: list'''
387        print '%u crash logs are loaded:' % len(self.crash_logs)
388        for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
389            print '[%u] = %s' % (crash_log_idx, crash_log.path)
390
391    def do_image(self, line):
392        '''Dump information about an image in the crash log given an image basename.
393
394        USAGE: image <basename>'''
395        usage = "usage: %prog [options] <PATH> [PATH ...]"
396        description='''Dump information about one or more images in all crash logs. The <PATH>
397        can be a full path or a image basename.'''
398        command_args = shlex.split(line)
399        if not self.image_option_parser:
400            self.image_option_parser = optparse.OptionParser(description=description, prog='image',usage=usage)
401            self.image_option_parser.add_option('-a', '--all', action='store_true', help='show all images', default=False)
402        try:
403            (options, args) = self.image_option_parser.parse_args(command_args)
404        except:
405            return
406
407        if args:
408            for image_path in args:
409                fullpath_search = image_path[0] == '/'
410                for crash_log in self.crash_logs:
411                    matches_found = 0
412                    for (image_idx, image) in enumerate(crash_log.images):
413                        if fullpath_search:
414                            if image.get_resolved_path() == image_path:
415                                matches_found += 1
416                                print image
417                        else:
418                            image_basename = image.get_resolved_path_basename()
419                            if image_basename == image_path:
420                                matches_found += 1
421                                print image
422                    if matches_found == 0:
423                        for (image_idx, image) in enumerate(crash_log.images):
424                            resolved_image_path = image.get_resolved_path()
425                            if resolved_image_path and string.find(image.get_resolved_path(), image_path) >= 0:
426                                print image
427        else:
428            for crash_log in self.crash_logs:
429                for (image_idx, image) in enumerate(crash_log.images):
430                    print '[%u] %s' % (image_idx, image)
431        return False
432
433
434def interactive_crashlogs(options, args):
435    crash_log_files = list()
436    for arg in args:
437        for resolved_path in glob.glob(arg):
438            crash_log_files.append(resolved_path)
439
440    crash_logs = list();
441    for crash_log_file in crash_log_files:
442        #print 'crash_log_file = "%s"' % crash_log_file
443        crash_log = CrashLog(crash_log_file)
444        if crash_log.error:
445            print crash_log.error
446            continue
447        if options.verbose:
448            crash_log.dump()
449        if not crash_log.images:
450            print 'error: no images in crash log "%s"' % (crash_log)
451            continue
452        else:
453            crash_logs.append(crash_log)
454
455    interpreter = Interactive(crash_logs)
456    # List all crash logs that were imported
457    interpreter.do_list()
458    interpreter.cmdloop()
459
460
461def Symbolicate(debugger, command, result, dict):
462    try:
463        SymbolicateCrashLog (shlex.split(command))
464    except:
465        result.PutCString ("error: python exception %s" % sys.exc_info()[0])
466
467def SymbolicateCrashLog(command_args):
468    usage = "usage: %prog [options] <FILE> [FILE ...]"
469    description='''Symbolicate one or more darwin crash log files to provide source file and line information,
470inlined stack frames back to the concrete functions, and disassemble the location of the crash
471for the first frame of the crashed thread.
472If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter
473for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been
474created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows
475you to explore the program as if it were stopped at the locations described in the crash log and functions can
476be disassembled and lookups can be performed using the addresses found in the crash log.'''
477    parser = optparse.OptionParser(description=description, prog='crashlog',usage=usage)
478    parser.add_option('-v', '--verbose', action='store_true', dest='verbose', help='display verbose debug info', default=False)
479    parser.add_option('-a', '--load-all', action='store_true', dest='load_all_images', help='load all executable images, not just the images found in the crashed stack frames', default=False)
480    parser.add_option('--images', action='store_true', dest='dump_image_list', help='show image list', default=False)
481    parser.add_option('-g', '--debug-delay', type='int', dest='debug_delay', metavar='NSEC', help='pause for NSEC seconds for debugger', default=0)
482    parser.add_option('-c', '--crashed-only', action='store_true', dest='crashed_only', help='only symbolicate the crashed thread', default=False)
483    parser.add_option('-d', '--disasm-depth', type='int', dest='disassemble_depth', help='set the depth in stack frames that should be disassembled (default is 1)', default=1)
484    parser.add_option('-D', '--disasm-all', action='store_true', dest='disassemble_all_threads', help='enabled disassembly of frames on all threads (not just the crashed thread)', default=False)
485    parser.add_option('-B', '--disasm-before', type='int', dest='disassemble_before', help='the number of instructions to disassemble before the frame PC', default=4)
486    parser.add_option('-A', '--disasm-after', type='int', dest='disassemble_after', help='the number of instructions to disassemble after the frame PC', default=4)
487    parser.add_option('-i', '--interactive', action='store_true', help='parse all crash logs and enter interactive mode', default=False)
488    try:
489        (options, args) = parser.parse_args(command_args)
490    except:
491        return
492
493    if options.verbose:
494        print 'command_args = %s' % command_args
495        print 'options', options
496        print 'args', args
497
498    if options.debug_delay > 0:
499        print "Waiting %u seconds for debugger to attach..." % options.debug_delay
500        time.sleep(options.debug_delay)
501    error = lldb.SBError()
502
503    if args:
504        if options.interactive:
505            interactive_crashlogs(options, args)
506        else:
507            for crash_log_file in args:
508                crash_log = CrashLog(crash_log_file)
509
510                #pp = pprint.PrettyPrinter(indent=4); pp.pprint(args)
511                if crash_log.error:
512                    print crash_log.error
513                    return
514                if options.verbose:
515                    crash_log.dump()
516                if not crash_log.images:
517                    print 'error: no images in crash log'
518                    return
519
520                target = crash_log.create_target ()
521                if not target:
522                    return
523                exe_module = target.GetModuleAtIndex(0)
524                images_to_load = list()
525                loaded_images = list()
526                if options.load_all_images:
527                    # --load-all option was specified, load everything up
528                    for image in crash_log.images:
529                        images_to_load.append(image)
530                else:
531                    # Only load the images found in stack frames for the crashed threads
532                    for ident in crash_log.idents:
533                        images = crash_log.find_images_with_identifier (ident)
534                        if images:
535                            for image in images:
536                                images_to_load.append(image)
537                        else:
538                            print 'error: can\'t find image for identifier "%s"' % ident
539
540                for image in images_to_load:
541                    if image in loaded_images:
542                        print "warning: skipping %s loaded at %#16.16x duplicate entry (probably commpage)" % (image.path, image.text_addr_lo)
543                    else:
544                        err = image.add_module (target)
545                        if err:
546                            print err
547                        else:
548                            #print 'loaded %s' % image
549                            loaded_images.append(image)
550
551                for thread in crash_log.threads:
552                    this_thread_crashed = thread.did_crash()
553                    if options.crashed_only and this_thread_crashed == False:
554                        continue
555                    print "%s" % thread
556                    #prev_frame_index = -1
557                    for frame_idx, frame in enumerate(thread.frames):
558                        disassemble = (this_thread_crashed or options.disassemble_all_threads) and frame_idx < options.disassemble_depth;
559                        if frame_idx == 0:
560                            symbolicated_frame_addresses = crash_log.symbolicate (frame.pc)
561                        else:
562                            # Any frame above frame zero and we have to subtract one to get the previous line entry
563                            symbolicated_frame_addresses = crash_log.symbolicate (frame.pc - 1)
564
565                        if symbolicated_frame_addresses:
566                            symbolicated_frame_address_idx = 0
567                            for symbolicated_frame_address in symbolicated_frame_addresses:
568                                print '[%3u] %s' % (frame_idx, symbolicated_frame_address)
569
570                                if symbolicated_frame_address_idx == 0:
571                                    if disassemble:
572                                        instructions = symbolicated_frame_address.get_instructions()
573                                        if instructions:
574                                            print
575                                            symbolication.disassemble_instructions (target,
576                                                                                    instructions,
577                                                                                    frame.pc,
578                                                                                    options.disassemble_before,
579                                                                                    options.disassemble_after, frame.index > 0)
580                                            print
581                                symbolicated_frame_address_idx += 1
582                        else:
583                            print frame
584                    print
585
586                if options.dump_image_list:
587                    print "Binary Images:"
588                    for image in crash_log.images:
589                        print image
590
591if __name__ == '__main__':
592    # Create a new debugger instance
593    lldb.debugger = lldb.SBDebugger.Create()
594    SymbolicateCrashLog (sys.argv[1:])
595elif getattr(lldb, 'debugger', None):
596    lldb.debugger.HandleCommand('command script add -f lldb.macosx.crashlog.Symbolicate crashlog')
597    print '"crashlog" command installed, type "crashlog --help" for detailed help'
598
599