Home | History | Annotate | Download | only in python
      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 
     29 import commands
     30 import cmd
     31 import datetime
     32 import glob
     33 import optparse
     34 import os
     35 import platform
     36 import plistlib
     37 import pprint # pp = pprint.PrettyPrinter(indent=4); pp.pprint(command_args)
     38 import re
     39 import shlex
     40 import string
     41 import sys
     42 import time
     43 import uuid
     44 
     45 try: 
     46     # Just try for LLDB in case PYTHONPATH is already correctly setup
     47     import lldb
     48 except ImportError:
     49     lldb_python_dirs = list()
     50     # lldb is not in the PYTHONPATH, try some defaults for the current platform
     51     platform_system = platform.system()
     52     if platform_system == 'Darwin':
     53         # On Darwin, try the currently selected Xcode directory
     54         xcode_dir = commands.getoutput("xcode-select --print-path")
     55         if xcode_dir:
     56             lldb_python_dirs.append(os.path.realpath(xcode_dir + '/../SharedFrameworks/LLDB.framework/Resources/Python'))
     57             lldb_python_dirs.append(xcode_dir + '/Library/PrivateFrameworks/LLDB.framework/Resources/Python')
     58         lldb_python_dirs.append('/System/Library/PrivateFrameworks/LLDB.framework/Resources/Python')
     59     success = False
     60     for lldb_python_dir in lldb_python_dirs:
     61         if os.path.exists(lldb_python_dir):
     62             if not (sys.path.__contains__(lldb_python_dir)):
     63                 sys.path.append(lldb_python_dir)
     64                 try: 
     65                     import lldb
     66                 except ImportError:
     67                     pass
     68                 else:
     69                     print 'imported lldb from: "%s"' % (lldb_python_dir)
     70                     success = True
     71                     break
     72     if not success:
     73         print "error: couldn't locate the 'lldb' module, please set PYTHONPATH correctly"
     74         sys.exit(1)
     75 
     76 from lldb.utils import symbolication
     77 
     78 PARSE_MODE_NORMAL = 0
     79 PARSE_MODE_THREAD = 1
     80 PARSE_MODE_IMAGES = 2
     81 PARSE_MODE_THREGS = 3
     82 PARSE_MODE_SYSTEM = 4
     83 
     84 class CrashLog(symbolication.Symbolicator):
     85     """Class that does parses darwin crash logs"""
     86     parent_process_regex = re.compile('^Parent Process:\s*(.*)\[(\d+)\]');
     87     thread_state_regex = re.compile('^Thread ([0-9]+) crashed with')
     88     thread_regex = re.compile('^Thread ([0-9]+)([^:]*):(.*)')
     89     frame_regex = re.compile('^([0-9]+) +([^ ]+) *\t?(0x[0-9a-fA-F]+) +(.*)')
     90     image_regex_uuid = re.compile('(0x[0-9a-fA-F]+)[- ]+(0x[0-9a-fA-F]+) +[+]?([^ ]+) +([^<]+)<([-0-9a-fA-F]+)> (.*)');
     91     image_regex_no_uuid = re.compile('(0x[0-9a-fA-F]+)[- ]+(0x[0-9a-fA-F]+) +[+]?([^ ]+) +([^/]+)/(.*)');
     92     empty_line_regex = re.compile('^$')
     93         
     94     class Thread:
     95         """Class that represents a thread in a darwin crash log"""
     96         def __init__(self, index):
     97             self.index = index
     98             self.frames = list()
     99             self.idents = list()
    100             self.registers = dict()
    101             self.reason = None
    102             self.queue = None
    103         
    104         def dump(self, prefix):
    105             print "%sThread[%u] %s" % (prefix, self.index, self.reason)
    106             if self.frames:
    107                 print "%s  Frames:" % (prefix)
    108                 for frame in self.frames:
    109                     frame.dump(prefix + '    ')
    110             if self.registers:
    111                 print "%s  Registers:" % (prefix)
    112                 for reg in self.registers.keys():
    113                     print "%s    %-5s = %#16.16x" % (prefix, reg, self.registers[reg])
    114         
    115         def add_ident(self, ident):
    116             if not ident in self.idents:
    117                 self.idents.append(ident)
    118             
    119         def did_crash(self):
    120             return self.reason != None
    121         
    122         def __str__(self):
    123             s = "Thread[%u]" % self.index
    124             if self.reason:
    125                 s += ' %s' % self.reason
    126             return s
    127         
    128     
    129     class Frame:
    130         """Class that represents a stack frame in a thread in a darwin crash log"""
    131         def __init__(self, index, pc, description):
    132             self.pc = pc
    133             self.description = description
    134             self.index = index
    135         
    136         def __str__(self):
    137             if self.description:
    138                 return "[%3u] 0x%16.16x %s" % (self.index, self.pc, self.description)
    139             else:
    140                 return "[%3u] 0x%16.16x" % (self.index, self.pc)
    141 
    142         def dump(self, prefix):
    143             print "%s%s" % (prefix, str(self))
    144     
    145     class DarwinImage(symbolication.Image):
    146         """Class that represents a binary images in a darwin crash log"""
    147         dsymForUUIDBinary = os.path.expanduser('~rc/bin/dsymForUUID')
    148         if not os.path.exists(dsymForUUIDBinary):
    149             dsymForUUIDBinary = commands.getoutput('which dsymForUUID')
    150             
    151         dwarfdump_uuid_regex = re.compile('UUID: ([-0-9a-fA-F]+) \(([^\(]+)\) .*')
    152         
    153         def __init__(self, text_addr_lo, text_addr_hi, identifier, version, uuid, path):
    154             symbolication.Image.__init__(self, path, uuid);
    155             self.add_section (symbolication.Section(text_addr_lo, text_addr_hi, "__TEXT"))
    156             self.identifier = identifier
    157             self.version = version
    158         
    159         def locate_module_and_debug_symbols(self):
    160             # Don't load a module twice...
    161             if self.resolved:
    162                 return True
    163             # Mark this as resolved so we don't keep trying
    164             self.resolved = True
    165             uuid_str = self.get_normalized_uuid_string()
    166             print 'Getting symbols for %s %s...' % (uuid_str, self.path),
    167             if os.path.exists(self.dsymForUUIDBinary):
    168                 dsym_for_uuid_command = '%s %s' % (self.dsymForUUIDBinary, uuid_str)
    169                 s = commands.getoutput(dsym_for_uuid_command)
    170                 if s:
    171                     plist_root = plistlib.readPlistFromString (s)
    172                     if plist_root:
    173                         plist = plist_root[uuid_str]
    174                         if plist:
    175                             if 'DBGArchitecture' in plist:
    176                                 self.arch = plist['DBGArchitecture']
    177                             if 'DBGDSYMPath' in plist:
    178                                 self.symfile = os.path.realpath(plist['DBGDSYMPath'])
    179                             if 'DBGSymbolRichExecutable' in plist:
    180                                 self.resolved_path = os.path.expanduser (plist['DBGSymbolRichExecutable'])
    181             if not self.resolved_path and os.path.exists(self.path):
    182                 dwarfdump_cmd_output = commands.getoutput('dwarfdump --uuid "%s"' % self.path)
    183                 self_uuid = self.get_uuid()
    184                 for line in dwarfdump_cmd_output.splitlines():
    185                     match = self.dwarfdump_uuid_regex.search (line)
    186                     if match:
    187                         dwarf_uuid_str = match.group(1)
    188                         dwarf_uuid = uuid.UUID(dwarf_uuid_str)
    189                         if self_uuid == dwarf_uuid:
    190                             self.resolved_path = self.path
    191                             self.arch = match.group(2)
    192                             break;
    193                 if not self.resolved_path:
    194                     self.unavailable = True
    195                     print "error\n    error: unable to locate '%s' with UUID %s" % (self.path, uuid_str)
    196                     return False
    197             if (self.resolved_path and os.path.exists(self.resolved_path)) or (self.path and os.path.exists(self.path)):
    198                 print 'ok'
    199                 # if self.resolved_path:
    200                 #     print '  exe = "%s"' % self.resolved_path 
    201                 # if self.symfile:
    202                 #     print ' dsym = "%s"' % self.symfile
    203                 return True
    204             else:
    205                 self.unavailable = True
    206             return False
    207         
    208     
    209         
    210     def __init__(self, path):
    211         """CrashLog constructor that take a path to a darwin crash log file"""
    212         symbolication.Symbolicator.__init__(self);
    213         self.path = os.path.expanduser(path);
    214         self.info_lines = list()
    215         self.system_profile = list()
    216         self.threads = list()
    217         self.idents = list() # A list of the required identifiers for doing all stack backtraces
    218         self.crashed_thread_idx = -1
    219         self.version = -1
    220         self.error = None
    221         # With possible initial component of ~ or ~user replaced by that user's home directory.
    222         try:
    223             f = open(self.path)
    224         except IOError:
    225             self.error = 'error: cannot open "%s"' % self.path
    226             return
    227 
    228         self.file_lines = f.read().splitlines()
    229         parse_mode = PARSE_MODE_NORMAL
    230         thread = None
    231         for line in self.file_lines:
    232             # print line
    233             line_len = len(line)
    234             if line_len == 0:
    235                 if thread:
    236                     if parse_mode == PARSE_MODE_THREAD:
    237                         if thread.index == self.crashed_thread_idx:
    238                             thread.reason = ''
    239                             if self.thread_exception:
    240                                 thread.reason += self.thread_exception
    241                             if self.thread_exception_data:
    242                                 thread.reason += " (%s)" % self.thread_exception_data                                
    243                         self.threads.append(thread)
    244                     thread = None
    245                 else:
    246                     # only append an extra empty line if the previous line 
    247                     # in the info_lines wasn't empty
    248                     if len(self.info_lines) > 0 and len(self.info_lines[-1]):
    249                         self.info_lines.append(line)
    250                 parse_mode = PARSE_MODE_NORMAL
    251                 # print 'PARSE_MODE_NORMAL'
    252             elif parse_mode == PARSE_MODE_NORMAL:
    253                 if line.startswith ('Process:'):
    254                     (self.process_name, pid_with_brackets) = line[8:].strip().split(' [')
    255                     self.process_id = pid_with_brackets.strip('[]')
    256                 elif line.startswith ('Path:'):
    257                     self.process_path = line[5:].strip()
    258                 elif line.startswith ('Identifier:'):
    259                     self.process_identifier = line[11:].strip()
    260                 elif line.startswith ('Version:'):
    261                     version_string = line[8:].strip()
    262                     matched_pair = re.search("(.+)\((.+)\)", version_string)
    263                     if matched_pair:
    264                         self.process_version = matched_pair.group(1)
    265                         self.process_compatability_version = matched_pair.group(2)
    266                     else:
    267                         self.process = version_string
    268                         self.process_compatability_version = version_string
    269                 elif self.parent_process_regex.search(line):
    270                     parent_process_match = self.parent_process_regex.search(line)
    271                     self.parent_process_name = parent_process_match.group(1)
    272                     self.parent_process_id = parent_process_match.group(2)
    273                 elif line.startswith ('Exception Type:'):
    274                     self.thread_exception = line[15:].strip()
    275                     continue
    276                 elif line.startswith ('Exception Codes:'):
    277                     self.thread_exception_data = line[16:].strip()
    278                     continue
    279                 elif line.startswith ('Crashed Thread:'):
    280                     self.crashed_thread_idx = int(line[15:].strip().split()[0])
    281                     continue
    282                 elif line.startswith ('Report Version:'):
    283                     self.version = int(line[15:].strip())
    284                     continue
    285                 elif line.startswith ('System Profile:'):
    286                     parse_mode = PARSE_MODE_SYSTEM
    287                     continue
    288                 elif (line.startswith ('Interval Since Last Report:') or
    289                       line.startswith ('Crashes Since Last Report:') or
    290                       line.startswith ('Per-App Interval Since Last Report:') or
    291                       line.startswith ('Per-App Crashes Since Last Report:') or
    292                       line.startswith ('Sleep/Wake UUID:') or
    293                       line.startswith ('Anonymous UUID:')):
    294                     # ignore these
    295                     continue  
    296                 elif line.startswith ('Thread'):
    297                     thread_state_match = self.thread_state_regex.search (line)
    298                     if thread_state_match:
    299                         thread_state_match = self.thread_regex.search (line)
    300                         thread_idx = int(thread_state_match.group(1))
    301                         parse_mode = PARSE_MODE_THREGS
    302                         thread = self.threads[thread_idx]
    303                     else:
    304                         thread_match = self.thread_regex.search (line)
    305                         if thread_match:
    306                             # print 'PARSE_MODE_THREAD'
    307                             parse_mode = PARSE_MODE_THREAD
    308                             thread_idx = int(thread_match.group(1))
    309                             thread = CrashLog.Thread(thread_idx)
    310                     continue
    311                 elif line.startswith ('Binary Images:'):
    312                     parse_mode = PARSE_MODE_IMAGES
    313                     continue
    314                 self.info_lines.append(line.strip())
    315             elif parse_mode == PARSE_MODE_THREAD:
    316                 if line.startswith ('Thread'):
    317                     continue
    318                 frame_match = self.frame_regex.search(line)
    319                 if frame_match:
    320                     ident = frame_match.group(2)
    321                     thread.add_ident(ident)
    322                     if not ident in self.idents:
    323                         self.idents.append(ident)
    324                     thread.frames.append (CrashLog.Frame(int(frame_match.group(1)), int(frame_match.group(3), 0), frame_match.group(4)))
    325                 else:
    326                     print 'error: frame regex failed for line: "%s"' % line
    327             elif parse_mode == PARSE_MODE_IMAGES:
    328                 image_match = self.image_regex_uuid.search (line)
    329                 if image_match:
    330                     image = CrashLog.DarwinImage (int(image_match.group(1),0), 
    331                                                   int(image_match.group(2),0), 
    332                                                   image_match.group(3).strip(), 
    333                                                   image_match.group(4).strip(), 
    334                                                   uuid.UUID(image_match.group(5)), 
    335                                                   image_match.group(6))
    336                     self.images.append (image)
    337                 else:
    338                     image_match = self.image_regex_no_uuid.search (line)
    339                     if image_match:
    340                         image = CrashLog.DarwinImage (int(image_match.group(1),0), 
    341                                                       int(image_match.group(2),0), 
    342                                                       image_match.group(3).strip(), 
    343                                                       image_match.group(4).strip(), 
    344                                                       None,
    345                                                       image_match.group(5))
    346                         self.images.append (image)
    347                     else:
    348                         print "error: image regex failed for: %s" % line
    349 
    350             elif parse_mode == PARSE_MODE_THREGS:
    351                 stripped_line = line.strip()
    352                 # "r12: 0x00007fff6b5939c8  r13: 0x0000000007000006  r14: 0x0000000000002a03  r15: 0x0000000000000c00"
    353                 reg_values = re.findall ('([a-zA-Z0-9]+: 0[Xx][0-9a-fA-F]+) *', stripped_line);
    354                 for reg_value in reg_values:
    355                     #print 'reg_value = "%s"' % reg_value
    356                     (reg, value) = reg_value.split(': ')
    357                     #print 'reg = "%s"' % reg
    358                     #print 'value = "%s"' % value
    359                     thread.registers[reg.strip()] = int(value, 0)
    360             elif parse_mode == PARSE_MODE_SYSTEM:
    361                 self.system_profile.append(line)
    362         f.close()
    363     
    364     def dump(self):
    365         print "Crash Log File: %s" % (self.path)
    366         print "\nThreads:"
    367         for thread in self.threads:
    368             thread.dump('  ')
    369         print "\nImages:"
    370         for image in self.images:
    371             image.dump('  ')
    372     
    373     def find_image_with_identifier(self, identifier):
    374         for image in self.images:
    375             if image.identifier == identifier:
    376                 return image
    377         return None
    378     
    379     def create_target(self):
    380         #print 'crashlog.create_target()...'
    381         target = symbolication.Symbolicator.create_target(self)
    382         if target:
    383             return target
    384         # We weren't able to open the main executable as, but we can still symbolicate
    385         print 'crashlog.create_target()...2'
    386         if self.idents:
    387             for ident in self.idents:
    388                 image = self.find_image_with_identifier (ident)
    389                 if image:
    390                     target = image.create_target ()
    391                     if target:
    392                         return target # success
    393         print 'crashlog.create_target()...3'
    394         for image in self.images:
    395             target = image.create_target ()
    396             if target:
    397                 return target # success
    398         print 'crashlog.create_target()...4'
    399         print 'error: unable to locate any executables from the crash log'
    400         return None
    401     
    402 
    403 def usage():
    404     print "Usage: lldb-symbolicate.py [-n name] executable-image"
    405     sys.exit(0)
    406 
    407 class Interactive(cmd.Cmd):
    408     '''Interactive prompt for analyzing one or more Darwin crash logs, type "help" to see a list of supported commands.'''
    409     image_option_parser = None
    410     
    411     def __init__(self, crash_logs):
    412         cmd.Cmd.__init__(self)
    413         self.use_rawinput = False
    414         self.intro = 'Interactive crashlogs prompt, type "help" to see a list of supported commands.'
    415         self.crash_logs = crash_logs
    416         self.prompt = '% '
    417 
    418     def default(self, line):
    419         '''Catch all for unknown command, which will exit the interpreter.'''
    420         print "uknown command: %s" % line
    421         return True
    422 
    423     def do_q(self, line):
    424         '''Quit command'''
    425         return True
    426 
    427     def do_quit(self, line):
    428         '''Quit command'''
    429         return True
    430 
    431     def do_symbolicate(self, line):
    432         description='''Symbolicate one or more darwin crash log files by index to provide source file and line information,
    433         inlined stack frames back to the concrete functions, and disassemble the location of the crash
    434         for the first frame of the crashed thread.'''
    435         option_parser = CreateSymbolicateCrashLogOptions ('symbolicate', description, False)
    436         command_args = shlex.split(line)
    437         try:
    438             (options, args) = option_parser.parse_args(command_args)
    439         except:
    440             return
    441 
    442         if args:
    443             # We have arguments, they must valid be crash log file indexes
    444             for idx_str in args:
    445                 idx = int(idx_str)
    446                 if idx < len(self.crash_logs):
    447                     SymbolicateCrashLog (self.crash_logs[idx], options)
    448                 else:
    449                     print 'error: crash log index %u is out of range' % (idx)
    450         else:
    451             # No arguments, symbolicate all crash logs using the options provided
    452             for idx in range(len(self.crash_logs)):
    453                 SymbolicateCrashLog (self.crash_logs[idx], options)                
    454     
    455     def do_list(self, line=None):
    456         '''Dump a list of all crash logs that are currently loaded.
    457         
    458         USAGE: list'''
    459         print '%u crash logs are loaded:' % len(self.crash_logs)
    460         for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
    461             print '[%u] = %s' % (crash_log_idx, crash_log.path)
    462 
    463     def do_image(self, line):
    464         '''Dump information about one or more binary images in the crash log given an image basename, or all images if no arguments are provided.'''
    465         usage = "usage: %prog [options] <PATH> [PATH ...]"
    466         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.'''
    467         command_args = shlex.split(line)
    468         if not self.image_option_parser:
    469             self.image_option_parser = optparse.OptionParser(description=description, prog='image',usage=usage)
    470             self.image_option_parser.add_option('-a', '--all', action='store_true', help='show all images', default=False)
    471         try:
    472             (options, args) = self.image_option_parser.parse_args(command_args)
    473         except:
    474             return
    475         
    476         if args:
    477             for image_path in args:
    478                 fullpath_search = image_path[0] == '/'
    479                 for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
    480                     matches_found = 0
    481                     for (image_idx, image) in enumerate(crash_log.images):
    482                         if fullpath_search:
    483                             if image.get_resolved_path() == image_path:
    484                                 matches_found += 1
    485                                 print '[%u] ' % (crash_log_idx), image
    486                         else:
    487                             image_basename = image.get_resolved_path_basename()
    488                             if image_basename == image_path:
    489                                 matches_found += 1
    490                                 print '[%u] ' % (crash_log_idx), image
    491                     if matches_found == 0:
    492                         for (image_idx, image) in enumerate(crash_log.images):
    493                             resolved_image_path = image.get_resolved_path()
    494                             if resolved_image_path and string.find(image.get_resolved_path(), image_path) >= 0:
    495                                 print '[%u] ' % (crash_log_idx), image
    496         else:
    497             for crash_log in self.crash_logs:
    498                 for (image_idx, image) in enumerate(crash_log.images):
    499                     print '[%u] %s' % (image_idx, image)            
    500         return False
    501 
    502 
    503 def interactive_crashlogs(options, args):
    504     crash_log_files = list()
    505     for arg in args:
    506         for resolved_path in glob.glob(arg):
    507             crash_log_files.append(resolved_path)
    508     
    509     crash_logs = list();
    510     for crash_log_file in crash_log_files:
    511         #print 'crash_log_file = "%s"' % crash_log_file
    512         crash_log = CrashLog(crash_log_file)
    513         if crash_log.error:
    514             print crash_log.error
    515             continue
    516         if options.debug:
    517             crash_log.dump()
    518         if not crash_log.images:
    519             print 'error: no images in crash log "%s"' % (crash_log)
    520             continue
    521         else:
    522             crash_logs.append(crash_log)
    523     
    524     interpreter = Interactive(crash_logs)
    525     # List all crash logs that were imported
    526     interpreter.do_list()
    527     interpreter.cmdloop()
    528     
    529 
    530 def save_crashlog(debugger, command, result, dict):
    531     usage = "usage: %prog [options] <output-path>"
    532     description='''Export the state of current target into a crashlog file'''
    533     parser = optparse.OptionParser(description=description, prog='save_crashlog',usage=usage)
    534     parser.add_option('-v', '--verbose', action='store_true', dest='verbose', help='display verbose debug info', default=False)
    535     try:
    536         (options, args) = parser.parse_args(shlex.split(command))
    537     except:
    538         result.PutCString ("error: invalid options");
    539         return
    540     if len(args) != 1:
    541         result.PutCString ("error: invalid arguments, a single output file is the only valid argument")
    542         return
    543     out_file = open(args[0], 'w')
    544     if not out_file:
    545         result.PutCString ("error: failed to open file '%s' for writing...", args[0]);
    546         return
    547     if lldb.target:
    548         identifier = lldb.target.executable.basename
    549         if lldb.process:
    550             pid = lldb.process.id
    551             if pid != lldb.LLDB_INVALID_PROCESS_ID:
    552                 out_file.write('Process:         %s [%u]\n' % (identifier, pid))
    553         out_file.write('Path:            %s\n' % (lldb.target.executable.fullpath))
    554         out_file.write('Identifier:      %s\n' % (identifier))
    555         out_file.write('\nDate/Time:       %s\n' % (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
    556         out_file.write('OS Version:      Mac OS X %s (%s)\n' % (platform.mac_ver()[0], commands.getoutput('sysctl -n kern.osversion')));
    557         out_file.write('Report Version:  9\n')
    558         for thread_idx in range(lldb.process.num_threads):
    559             thread = lldb.process.thread[thread_idx]
    560             out_file.write('\nThread %u:\n' % (thread_idx))
    561             for (frame_idx, frame) in enumerate(thread.frames):
    562                 frame_pc = frame.pc
    563                 frame_offset = 0
    564                 if frame.function:
    565                     block = frame.GetFrameBlock()
    566                     block_range = block.range[frame.addr]
    567                     if block_range:
    568                         block_start_addr = block_range[0]
    569                         frame_offset = frame_pc - block_start_addr.load_addr
    570                     else:
    571                         frame_offset = frame_pc - frame.function.addr.load_addr
    572                 elif frame.symbol:
    573                     frame_offset = frame_pc - frame.symbol.addr.load_addr
    574                 out_file.write('%-3u %-32s 0x%16.16x %s' % (frame_idx, frame.module.file.basename, frame_pc, frame.name))
    575                 if frame_offset > 0: 
    576                     out_file.write(' + %u' % (frame_offset))
    577                 line_entry = frame.line_entry
    578                 if line_entry:
    579                     if options.verbose:
    580                         # This will output the fullpath + line + column
    581                         out_file.write(' %s' % (line_entry))
    582                     else:
    583                         out_file.write(' %s:%u' % (line_entry.file.basename, line_entry.line))
    584                         column = line_entry.column
    585                         if column: 
    586                             out_file.write(':%u' % (column))
    587                 out_file.write('\n')
    588                 
    589         out_file.write('\nBinary Images:\n')
    590         for module in lldb.target.modules:
    591             text_segment = module.section['__TEXT']
    592             if text_segment:
    593                 text_segment_load_addr = text_segment.GetLoadAddress(lldb.target)
    594                 if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS:
    595                     text_segment_end_load_addr = text_segment_load_addr + text_segment.size
    596                     identifier = module.file.basename
    597                     module_version = '???'
    598                     module_version_array = module.GetVersion()
    599                     if module_version_array:
    600                         module_version = '.'.join(map(str,module_version_array))
    601                     out_file.write ('    0x%16.16x - 0x%16.16x  %s (%s - ???) <%s> %s\n' % (text_segment_load_addr, text_segment_end_load_addr, identifier, module_version, module.GetUUIDString(), module.file.fullpath))
    602         out_file.close()
    603     else:
    604         result.PutCString ("error: invalid target");
    605         
    606     
    607 def Symbolicate(debugger, command, result, dict):
    608     try:
    609         SymbolicateCrashLogs (shlex.split(command))
    610     except:
    611         result.PutCString ("error: python exception %s" % sys.exc_info()[0])
    612 
    613 def SymbolicateCrashLog(crash_log, options):
    614     if crash_log.error:
    615         print crash_log.error
    616         return
    617     if options.debug:
    618         crash_log.dump()
    619     if not crash_log.images:
    620         print 'error: no images in crash log'
    621         return
    622 
    623     if options.dump_image_list:
    624         print "Binary Images:"
    625         for image in crash_log.images:
    626             if options.verbose:
    627                 print image.debug_dump()
    628             else:
    629                 print image
    630 
    631     target = crash_log.create_target ()
    632     if not target:
    633         return
    634     exe_module = target.GetModuleAtIndex(0)
    635     images_to_load = list()
    636     loaded_images = list()
    637     if options.load_all_images:
    638         # --load-all option was specified, load everything up
    639         for image in crash_log.images:
    640             images_to_load.append(image)
    641     else:
    642         # Only load the images found in stack frames for the crashed threads
    643         if options.crashed_only:
    644             for thread in crash_log.threads:
    645                 if thread.did_crash():
    646                     for ident in thread.idents:
    647                         images = crash_log.find_images_with_identifier (ident)
    648                         if images:
    649                             for image in images:
    650                                 images_to_load.append(image)
    651                         else:
    652                             print 'error: can\'t find image for identifier "%s"' % ident
    653         else:
    654             for ident in crash_log.idents:
    655                 images = crash_log.find_images_with_identifier (ident)
    656                 if images:
    657                     for image in images:
    658                         images_to_load.append(image)
    659                 else:
    660                     print 'error: can\'t find image for identifier "%s"' % ident
    661 
    662     for image in images_to_load:
    663         if image in loaded_images:
    664             print "warning: skipping %s loaded at %#16.16x duplicate entry (probably commpage)" % (image.path, image.text_addr_lo)
    665         else:
    666             err = image.add_module (target)
    667             if err:
    668                 print err
    669             else:
    670                 #print 'loaded %s' % image
    671                 loaded_images.append(image)
    672 
    673     for thread in crash_log.threads:
    674         this_thread_crashed = thread.did_crash()
    675         if options.crashed_only and this_thread_crashed == False:
    676             continue
    677         print "%s" % thread
    678         #prev_frame_index = -1
    679         display_frame_idx = -1
    680         for frame_idx, frame in enumerate(thread.frames):
    681             disassemble = (this_thread_crashed or options.disassemble_all_threads) and frame_idx < options.disassemble_depth;
    682             if frame_idx == 0:
    683                 symbolicated_frame_addresses = crash_log.symbolicate (frame.pc & crash_log.addr_mask, options.verbose)
    684             else:
    685                 # Any frame above frame zero and we have to subtract one to get the previous line entry
    686                 symbolicated_frame_addresses = crash_log.symbolicate ((frame.pc & crash_log.addr_mask) - 1, options.verbose)
    687             
    688             if symbolicated_frame_addresses:
    689                 symbolicated_frame_address_idx = 0
    690                 for symbolicated_frame_address in symbolicated_frame_addresses:
    691                     display_frame_idx += 1
    692                     print '[%3u] %s' % (frame_idx, symbolicated_frame_address)
    693                     if (options.source_all or thread.did_crash()) and display_frame_idx < options.source_frames and options.source_context:
    694                         source_context = options.source_context
    695                         line_entry = symbolicated_frame_address.get_symbol_context().line_entry
    696                         if line_entry.IsValid():
    697                             strm = lldb.SBStream()
    698                             if line_entry:
    699                                 lldb.debugger.GetSourceManager().DisplaySourceLinesWithLineNumbers(line_entry.file, line_entry.line, source_context, source_context, "->", strm)
    700                             source_text = strm.GetData()
    701                             if source_text:
    702                                 # Indent the source a bit
    703                                 indent_str = '    '
    704                                 join_str = '\n' + indent_str
    705                                 print '%s%s' % (indent_str, join_str.join(source_text.split('\n')))
    706                     if symbolicated_frame_address_idx == 0:
    707                         if disassemble:
    708                             instructions = symbolicated_frame_address.get_instructions()
    709                             if instructions:
    710                                 print
    711                                 symbolication.disassemble_instructions (target, 
    712                                                                         instructions, 
    713                                                                         frame.pc, 
    714                                                                         options.disassemble_before, 
    715                                                                         options.disassemble_after, frame.index > 0)
    716                                 print
    717                     symbolicated_frame_address_idx += 1
    718             else:
    719                 print frame
    720         print                
    721 
    722 def CreateSymbolicateCrashLogOptions(command_name, description, add_interactive_options):
    723     usage = "usage: %prog [options] <FILE> [FILE ...]"
    724     option_parser = optparse.OptionParser(description=description, prog='crashlog',usage=usage)
    725     option_parser.add_option('--verbose'       , '-v', action='store_true', dest='verbose', help='display verbose debug info', default=False)
    726     option_parser.add_option('--debug'         , '-g', action='store_true', dest='debug', help='display verbose debug logging', default=False)
    727     option_parser.add_option('--load-all'      , '-a', action='store_true', dest='load_all_images', help='load all executable images, not just the images found in the crashed stack frames', default=False)
    728     option_parser.add_option('--images'        ,       action='store_true', dest='dump_image_list', help='show image list', default=False)
    729     option_parser.add_option('--debug-delay'   ,       type='int', dest='debug_delay', metavar='NSEC', help='pause for NSEC seconds for debugger', default=0)
    730     option_parser.add_option('--crashed-only'  , '-c', action='store_true', dest='crashed_only', help='only symbolicate the crashed thread', default=False)
    731     option_parser.add_option('--disasm-depth'  , '-d', type='int', dest='disassemble_depth', help='set the depth in stack frames that should be disassembled (default is 1)', default=1)
    732     option_parser.add_option('--disasm-all'    , '-D',  action='store_true', dest='disassemble_all_threads', help='enabled disassembly of frames on all threads (not just the crashed thread)', default=False)
    733     option_parser.add_option('--disasm-before' , '-B', type='int', dest='disassemble_before', help='the number of instructions to disassemble before the frame PC', default=4)
    734     option_parser.add_option('--disasm-after'  , '-A', type='int', dest='disassemble_after', help='the number of instructions to disassemble after the frame PC', default=4)
    735     option_parser.add_option('--source-context', '-C', type='int', metavar='NLINES', dest='source_context', help='show NLINES source lines of source context (default = 4)', default=4)
    736     option_parser.add_option('--source-frames' ,       type='int', metavar='NFRAMES', dest='source_frames', help='show source for NFRAMES (default = 4)', default=4)
    737     option_parser.add_option('--source-all'    ,       action='store_true', dest='source_all', help='show source for all threads, not just the crashed thread', default=False)
    738     if add_interactive_options:
    739         option_parser.add_option('-i', '--interactive', action='store_true', help='parse all crash logs and enter interactive mode', default=False)
    740     return option_parser
    741     
    742 def SymbolicateCrashLogs(command_args):
    743     description='''Symbolicate one or more darwin crash log files to provide source file and line information,
    744 inlined stack frames back to the concrete functions, and disassemble the location of the crash
    745 for the first frame of the crashed thread.
    746 If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter
    747 for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been
    748 created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows
    749 you to explore the program as if it were stopped at the locations described in the crash log and functions can 
    750 be disassembled and lookups can be performed using the addresses found in the crash log.'''
    751     option_parser = CreateSymbolicateCrashLogOptions ('crashlog', description, True)
    752     try:
    753         (options, args) = option_parser.parse_args(command_args)
    754     except:
    755         return
    756         
    757     if options.debug:
    758         print 'command_args = %s' % command_args
    759         print 'options', options
    760         print 'args', args
    761         
    762     if options.debug_delay > 0:
    763         print "Waiting %u seconds for debugger to attach..." % options.debug_delay
    764         time.sleep(options.debug_delay)
    765     error = lldb.SBError()
    766         
    767     if args:
    768         if options.interactive:
    769             interactive_crashlogs(options, args)
    770         else:
    771             for crash_log_file in args:
    772                 crash_log = CrashLog(crash_log_file)
    773                 SymbolicateCrashLog (crash_log, options)
    774 if __name__ == '__main__':
    775     # Create a new debugger instance
    776     lldb.debugger = lldb.SBDebugger.Create()
    777     SymbolicateCrashLogs (sys.argv[1:])
    778 elif getattr(lldb, 'debugger', None):
    779     lldb.debugger.HandleCommand('command script add -f lldb.macosx.crashlog.Symbolicate crashlog')
    780     lldb.debugger.HandleCommand('command script add -f lldb.macosx.crashlog.save_crashlog save_crashlog')
    781     print '"crashlog" and "save_crashlog" command installed, use the "--help" option for detailed help'
    782 
    783