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