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