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