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