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 29from __future__ import print_function 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 subprocess 42import sys 43import time 44import uuid 45 46try: 47 # Just try for LLDB in case PYTHONPATH is already correctly setup 48 import lldb 49except ImportError: 50 lldb_python_dirs = list() 51 # lldb is not in the PYTHONPATH, try some defaults for the current platform 52 platform_system = platform.system() 53 if platform_system == 'Darwin': 54 # On Darwin, try the currently selected Xcode directory 55 xcode_dir = subprocess.check_output("xcode-select --print-path", shell=True) 56 if xcode_dir: 57 lldb_python_dirs.append( 58 os.path.realpath( 59 xcode_dir + 60 '/../SharedFrameworks/LLDB.framework/Resources/Python')) 61 lldb_python_dirs.append( 62 xcode_dir + '/Library/PrivateFrameworks/LLDB.framework/Resources/Python') 63 lldb_python_dirs.append( 64 '/System/Library/PrivateFrameworks/LLDB.framework/Resources/Python') 65 success = False 66 for lldb_python_dir in lldb_python_dirs: 67 if os.path.exists(lldb_python_dir): 68 if not (sys.path.__contains__(lldb_python_dir)): 69 sys.path.append(lldb_python_dir) 70 try: 71 import lldb 72 except ImportError: 73 pass 74 else: 75 print('imported lldb from: "%s"' % (lldb_python_dir)) 76 success = True 77 break 78 if not success: 79 print("error: couldn't locate the 'lldb' module, please set PYTHONPATH correctly") 80 sys.exit(1) 81 82from lldb.utils import symbolication 83 84PARSE_MODE_NORMAL = 0 85PARSE_MODE_THREAD = 1 86PARSE_MODE_IMAGES = 2 87PARSE_MODE_THREGS = 3 88PARSE_MODE_SYSTEM = 4 89 90 91class CrashLog(symbolication.Symbolicator): 92 """Class that does parses darwin crash logs""" 93 parent_process_regex = re.compile('^Parent Process:\s*(.*)\[(\d+)\]') 94 thread_state_regex = re.compile('^Thread ([0-9]+) crashed with') 95 thread_regex = re.compile('^Thread ([0-9]+)([^:]*):(.*)') 96 app_backtrace_regex = re.compile( 97 '^Application Specific Backtrace ([0-9]+)([^:]*):(.*)') 98 frame_regex = re.compile('^([0-9]+)\s+(.+?)\s+(0x[0-9a-fA-F]{7}[0-9a-fA-F]+) +(.*)') 99 image_regex_uuid = re.compile( 100 '(0x[0-9a-fA-F]+)[-\s]+(0x[0-9a-fA-F]+)\s+[+]?(.+?)\s+(\(.+\))?\s?(<([-0-9a-fA-F]+)>)? (.*)') 101 empty_line_regex = re.compile('^$') 102 103 class Thread: 104 """Class that represents a thread in a darwin crash log""" 105 106 def __init__(self, index, app_specific_backtrace): 107 self.index = index 108 self.frames = list() 109 self.idents = list() 110 self.registers = dict() 111 self.reason = None 112 self.queue = None 113 self.app_specific_backtrace = app_specific_backtrace 114 115 def dump(self, prefix): 116 if self.app_specific_backtrace: 117 print("%Application Specific Backtrace[%u] %s" % (prefix, self.index, self.reason)) 118 else: 119 print("%sThread[%u] %s" % (prefix, self.index, self.reason)) 120 if self.frames: 121 print("%s Frames:" % (prefix)) 122 for frame in self.frames: 123 frame.dump(prefix + ' ') 124 if self.registers: 125 print("%s Registers:" % (prefix)) 126 for reg in self.registers.keys(): 127 print("%s %-5s = %#16.16x" % (prefix, reg, self.registers[reg])) 128 129 def dump_symbolicated(self, crash_log, options): 130 this_thread_crashed = self.app_specific_backtrace 131 if not this_thread_crashed: 132 this_thread_crashed = self.did_crash() 133 if options.crashed_only and this_thread_crashed == False: 134 return 135 136 print("%s" % self) 137 #prev_frame_index = -1 138 display_frame_idx = -1 139 for frame_idx, frame in enumerate(self.frames): 140 disassemble = ( 141 this_thread_crashed or options.disassemble_all_threads) and frame_idx < options.disassemble_depth 142 if frame_idx == 0: 143 symbolicated_frame_addresses = crash_log.symbolicate( 144 frame.pc & crash_log.addr_mask, options.verbose) 145 else: 146 # Any frame above frame zero and we have to subtract one to 147 # get the previous line entry 148 symbolicated_frame_addresses = crash_log.symbolicate( 149 (frame.pc & crash_log.addr_mask) - 1, options.verbose) 150 151 if symbolicated_frame_addresses: 152 symbolicated_frame_address_idx = 0 153 for symbolicated_frame_address in symbolicated_frame_addresses: 154 display_frame_idx += 1 155 print('[%3u] %s' % (frame_idx, symbolicated_frame_address)) 156 if (options.source_all or self.did_crash( 157 )) and display_frame_idx < options.source_frames and options.source_context: 158 source_context = options.source_context 159 line_entry = symbolicated_frame_address.get_symbol_context().line_entry 160 if line_entry.IsValid(): 161 strm = lldb.SBStream() 162 if line_entry: 163 lldb.debugger.GetSourceManager().DisplaySourceLinesWithLineNumbers( 164 line_entry.file, line_entry.line, source_context, source_context, "->", strm) 165 source_text = strm.GetData() 166 if source_text: 167 # Indent the source a bit 168 indent_str = ' ' 169 join_str = '\n' + indent_str 170 print('%s%s' % (indent_str, join_str.join(source_text.split('\n')))) 171 if symbolicated_frame_address_idx == 0: 172 if disassemble: 173 instructions = symbolicated_frame_address.get_instructions() 174 if instructions: 175 print() 176 symbolication.disassemble_instructions( 177 crash_log.get_target(), 178 instructions, 179 frame.pc, 180 options.disassemble_before, 181 options.disassemble_after, 182 frame.index > 0) 183 print() 184 symbolicated_frame_address_idx += 1 185 else: 186 print(frame) 187 188 def add_ident(self, ident): 189 if ident not in self.idents: 190 self.idents.append(ident) 191 192 def did_crash(self): 193 return self.reason is not None 194 195 def __str__(self): 196 if self.app_specific_backtrace: 197 s = "Application Specific Backtrace[%u]" % self.index 198 else: 199 s = "Thread[%u]" % self.index 200 if self.reason: 201 s += ' %s' % self.reason 202 return s 203 204 class Frame: 205 """Class that represents a stack frame in a thread in a darwin crash log""" 206 207 def __init__(self, index, pc, description): 208 self.pc = pc 209 self.description = description 210 self.index = index 211 212 def __str__(self): 213 if self.description: 214 return "[%3u] 0x%16.16x %s" % ( 215 self.index, self.pc, self.description) 216 else: 217 return "[%3u] 0x%16.16x" % (self.index, self.pc) 218 219 def dump(self, prefix): 220 print("%s%s" % (prefix, str(self))) 221 222 class DarwinImage(symbolication.Image): 223 """Class that represents a binary images in a darwin crash log""" 224 dsymForUUIDBinary = '/usr/local/bin/dsymForUUID' 225 if not os.path.exists(dsymForUUIDBinary): 226 try: 227 dsymForUUIDBinary = subprocess.check_output('which dsymForUUID', 228 shell=True).rstrip('\n') 229 except: 230 dsymForUUIDBinary = "" 231 232 dwarfdump_uuid_regex = re.compile( 233 'UUID: ([-0-9a-fA-F]+) \(([^\(]+)\) .*') 234 235 def __init__( 236 self, 237 text_addr_lo, 238 text_addr_hi, 239 identifier, 240 version, 241 uuid, 242 path): 243 symbolication.Image.__init__(self, path, uuid) 244 self.add_section( 245 symbolication.Section( 246 text_addr_lo, 247 text_addr_hi, 248 "__TEXT")) 249 self.identifier = identifier 250 self.version = version 251 252 def find_matching_slice(self): 253 dwarfdump_cmd_output = subprocess.check_output( 254 'dwarfdump --uuid "%s"' % self.path, shell=True) 255 self_uuid = self.get_uuid() 256 for line in dwarfdump_cmd_output.splitlines(): 257 match = self.dwarfdump_uuid_regex.search(line) 258 if match: 259 dwarf_uuid_str = match.group(1) 260 dwarf_uuid = uuid.UUID(dwarf_uuid_str) 261 if self_uuid == dwarf_uuid: 262 self.resolved_path = self.path 263 self.arch = match.group(2) 264 return True 265 if not self.resolved_path: 266 self.unavailable = True 267 print(("error\n error: unable to locate '%s' with UUID %s" 268 % (self.path, self.get_normalized_uuid_string()))) 269 return False 270 271 def locate_module_and_debug_symbols(self): 272 # Don't load a module twice... 273 if self.resolved: 274 return True 275 # Mark this as resolved so we don't keep trying 276 self.resolved = True 277 uuid_str = self.get_normalized_uuid_string() 278 print('Getting symbols for %s %s...' % (uuid_str, self.path), end=' ') 279 if os.path.exists(self.dsymForUUIDBinary): 280 dsym_for_uuid_command = '%s %s' % ( 281 self.dsymForUUIDBinary, uuid_str) 282 s = subprocess.check_output(dsym_for_uuid_command, shell=True) 283 if s: 284 try: 285 plist_root = plistlib.readPlistFromString(s) 286 except: 287 print(("Got exception: ", sys.exc_info()[1], " handling dsymForUUID output: \n", s)) 288 raise 289 if plist_root: 290 plist = plist_root[uuid_str] 291 if plist: 292 if 'DBGArchitecture' in plist: 293 self.arch = plist['DBGArchitecture'] 294 if 'DBGDSYMPath' in plist: 295 self.symfile = os.path.realpath( 296 plist['DBGDSYMPath']) 297 if 'DBGSymbolRichExecutable' in plist: 298 self.path = os.path.expanduser( 299 plist['DBGSymbolRichExecutable']) 300 self.resolved_path = self.path 301 if not self.resolved_path and os.path.exists(self.path): 302 if not self.find_matching_slice(): 303 return False 304 if not self.resolved_path and not os.path.exists(self.path): 305 try: 306 dsym = subprocess.check_output( 307 ["/usr/bin/mdfind", 308 "com_apple_xcode_dsym_uuids == %s"%uuid_str])[:-1] 309 if dsym and os.path.exists(dsym): 310 print(('falling back to binary inside "%s"'%dsym)) 311 self.symfile = dsym 312 dwarf_dir = os.path.join(dsym, 'Contents/Resources/DWARF') 313 for filename in os.listdir(dwarf_dir): 314 self.path = os.path.join(dwarf_dir, filename) 315 if not self.find_matching_slice(): 316 return False 317 break 318 except: 319 pass 320 if (self.resolved_path and os.path.exists(self.resolved_path)) or ( 321 self.path and os.path.exists(self.path)): 322 print('ok') 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], subprocess.check_output('sysctl -n kern.osversion', shell=True))) 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