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