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 = os.path.expanduser('~rc/bin/dsymForUUID') 225 if not os.path.exists(dsymForUUIDBinary): 226 try: 227 dsymForUUIDBinary = subprocess.check_output('which dsymForUUID', 228 shell=True) 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_value, " 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 import subprocess 307 dsym = subprocess.check_output( 308 ["/usr/bin/mdfind", 309 "com_apple_xcode_dsym_uuids == %s"%uuid_str])[:-1] 310 if dsym and os.path.exists(dsym): 311 print(('falling back to binary inside "%s"'%dsym)) 312 self.symfile = dsym 313 dwarf_dir = os.path.join(dsym, 'Contents/Resources/DWARF') 314 for filename in os.listdir(dwarf_dir): 315 self.path = os.path.join(dwarf_dir, filename) 316 if not self.find_matching_slice(): 317 return False 318 break 319 except: 320 pass 321 if (self.resolved_path and os.path.exists(self.resolved_path)) or ( 322 self.path and os.path.exists(self.path)): 323 print('ok') 324 # if self.resolved_path: 325 # print ' exe = "%s"' % self.resolved_path 326 # if self.symfile: 327 # print ' dsym = "%s"' % self.symfile 328 return True 329 else: 330 self.unavailable = True 331 return False 332 333 def __init__(self, path): 334 """CrashLog constructor that take a path to a darwin crash log file""" 335 symbolication.Symbolicator.__init__(self) 336 self.path = os.path.expanduser(path) 337 self.info_lines = list() 338 self.system_profile = list() 339 self.threads = list() 340 self.backtraces = list() # For application specific backtraces 341 self.idents = list() # A list of the required identifiers for doing all stack backtraces 342 self.crashed_thread_idx = -1 343 self.version = -1 344 self.error = None 345 self.target = None 346 # With possible initial component of ~ or ~user replaced by that user's 347 # home directory. 348 try: 349 f = open(self.path) 350 except IOError: 351 self.error = 'error: cannot open "%s"' % self.path 352 return 353 354 self.file_lines = f.read().splitlines() 355 parse_mode = PARSE_MODE_NORMAL 356 thread = None 357 app_specific_backtrace = False 358 for line in self.file_lines: 359 # print line 360 line_len = len(line) 361 if line_len == 0: 362 if thread: 363 if parse_mode == PARSE_MODE_THREAD: 364 if thread.index == self.crashed_thread_idx: 365 thread.reason = '' 366 if self.thread_exception: 367 thread.reason += self.thread_exception 368 if self.thread_exception_data: 369 thread.reason += " (%s)" % self.thread_exception_data 370 if app_specific_backtrace: 371 self.backtraces.append(thread) 372 else: 373 self.threads.append(thread) 374 thread = None 375 else: 376 # only append an extra empty line if the previous line 377 # in the info_lines wasn't empty 378 if len(self.info_lines) > 0 and len(self.info_lines[-1]): 379 self.info_lines.append(line) 380 parse_mode = PARSE_MODE_NORMAL 381 # print 'PARSE_MODE_NORMAL' 382 elif parse_mode == PARSE_MODE_NORMAL: 383 if line.startswith('Process:'): 384 (self.process_name, pid_with_brackets) = line[ 385 8:].strip().split(' [') 386 self.process_id = pid_with_brackets.strip('[]') 387 elif line.startswith('Path:'): 388 self.process_path = line[5:].strip() 389 elif line.startswith('Identifier:'): 390 self.process_identifier = line[11:].strip() 391 elif line.startswith('Version:'): 392 version_string = line[8:].strip() 393 matched_pair = re.search("(.+)\((.+)\)", version_string) 394 if matched_pair: 395 self.process_version = matched_pair.group(1) 396 self.process_compatability_version = matched_pair.group( 397 2) 398 else: 399 self.process = version_string 400 self.process_compatability_version = version_string 401 elif self.parent_process_regex.search(line): 402 parent_process_match = self.parent_process_regex.search( 403 line) 404 self.parent_process_name = parent_process_match.group(1) 405 self.parent_process_id = parent_process_match.group(2) 406 elif line.startswith('Exception Type:'): 407 self.thread_exception = line[15:].strip() 408 continue 409 elif line.startswith('Exception Codes:'): 410 self.thread_exception_data = line[16:].strip() 411 continue 412 elif line.startswith('Exception Subtype:'): # iOS 413 self.thread_exception_data = line[18:].strip() 414 continue 415 elif line.startswith('Crashed Thread:'): 416 self.crashed_thread_idx = int(line[15:].strip().split()[0]) 417 continue 418 elif line.startswith('Triggered by Thread:'): # iOS 419 self.crashed_thread_idx = int(line[20:].strip().split()[0]) 420 continue 421 elif line.startswith('Report Version:'): 422 self.version = int(line[15:].strip()) 423 continue 424 elif line.startswith('System Profile:'): 425 parse_mode = PARSE_MODE_SYSTEM 426 continue 427 elif (line.startswith('Interval Since Last Report:') or 428 line.startswith('Crashes Since Last Report:') or 429 line.startswith('Per-App Interval Since Last Report:') or 430 line.startswith('Per-App Crashes Since Last Report:') or 431 line.startswith('Sleep/Wake UUID:') or 432 line.startswith('Anonymous UUID:')): 433 # ignore these 434 continue 435 elif line.startswith('Thread'): 436 thread_state_match = self.thread_state_regex.search(line) 437 if thread_state_match: 438 app_specific_backtrace = False 439 thread_state_match = self.thread_regex.search(line) 440 thread_idx = int(thread_state_match.group(1)) 441 parse_mode = PARSE_MODE_THREGS 442 thread = self.threads[thread_idx] 443 else: 444 thread_match = self.thread_regex.search(line) 445 if thread_match: 446 app_specific_backtrace = False 447 parse_mode = PARSE_MODE_THREAD 448 thread_idx = int(thread_match.group(1)) 449 thread = CrashLog.Thread(thread_idx, False) 450 continue 451 elif line.startswith('Binary Images:'): 452 parse_mode = PARSE_MODE_IMAGES 453 continue 454 elif line.startswith('Application Specific Backtrace'): 455 app_backtrace_match = self.app_backtrace_regex.search(line) 456 if app_backtrace_match: 457 parse_mode = PARSE_MODE_THREAD 458 app_specific_backtrace = True 459 idx = int(app_backtrace_match.group(1)) 460 thread = CrashLog.Thread(idx, True) 461 elif line.startswith('Last Exception Backtrace:'): # iOS 462 parse_mode = PARSE_MODE_THREAD 463 app_specific_backtrace = True 464 idx = 1 465 thread = CrashLog.Thread(idx, True) 466 self.info_lines.append(line.strip()) 467 elif parse_mode == PARSE_MODE_THREAD: 468 if line.startswith('Thread'): 469 continue 470 frame_match = self.frame_regex.search(line) 471 if frame_match: 472 ident = frame_match.group(2) 473 thread.add_ident(ident) 474 if ident not in self.idents: 475 self.idents.append(ident) 476 thread.frames.append(CrashLog.Frame(int(frame_match.group(1)), int( 477 frame_match.group(3), 0), frame_match.group(4))) 478 else: 479 print('error: frame regex failed for line: "%s"' % line) 480 elif parse_mode == PARSE_MODE_IMAGES: 481 image_match = self.image_regex_uuid.search(line) 482 if image_match: 483 (img_lo, img_hi, img_name, img_version, 484 _, img_uuid, img_path) = image_match.groups() 485 image = CrashLog.DarwinImage(int(img_lo, 0), int(img_hi, 0), 486 img_name.strip(), 487 img_version.strip() 488 if img_version else "", 489 uuid.UUID(img_uuid), img_path) 490 self.images.append(image) 491 else: 492 print("error: image regex failed for: %s" % line) 493 494 elif parse_mode == PARSE_MODE_THREGS: 495 stripped_line = line.strip() 496 # "r12: 0x00007fff6b5939c8 r13: 0x0000000007000006 r14: 0x0000000000002a03 r15: 0x0000000000000c00" 497 reg_values = re.findall( 498 '([a-zA-Z0-9]+: 0[Xx][0-9a-fA-F]+) *', stripped_line) 499 for reg_value in reg_values: 500 # print 'reg_value = "%s"' % reg_value 501 (reg, value) = reg_value.split(': ') 502 # print 'reg = "%s"' % reg 503 # print 'value = "%s"' % value 504 thread.registers[reg.strip()] = int(value, 0) 505 elif parse_mode == PARSE_MODE_SYSTEM: 506 self.system_profile.append(line) 507 f.close() 508 509 def dump(self): 510 print("Crash Log File: %s" % (self.path)) 511 if self.backtraces: 512 print("\nApplication Specific Backtraces:") 513 for thread in self.backtraces: 514 thread.dump(' ') 515 print("\nThreads:") 516 for thread in self.threads: 517 thread.dump(' ') 518 print("\nImages:") 519 for image in self.images: 520 image.dump(' ') 521 522 def find_image_with_identifier(self, identifier): 523 for image in self.images: 524 if image.identifier == identifier: 525 return image 526 regex_text = '^.*\.%s$' % (re.escape(identifier)) 527 regex = re.compile(regex_text) 528 for image in self.images: 529 if regex.match(image.identifier): 530 return image 531 return None 532 533 def create_target(self): 534 # print 'crashlog.create_target()...' 535 if self.target is None: 536 self.target = symbolication.Symbolicator.create_target(self) 537 if self.target: 538 return self.target 539 # We weren't able to open the main executable as, but we can still 540 # symbolicate 541 print('crashlog.create_target()...2') 542 if self.idents: 543 for ident in self.idents: 544 image = self.find_image_with_identifier(ident) 545 if image: 546 self.target = image.create_target() 547 if self.target: 548 return self.target # success 549 print('crashlog.create_target()...3') 550 for image in self.images: 551 self.target = image.create_target() 552 if self.target: 553 return self.target # success 554 print('crashlog.create_target()...4') 555 print('error: unable to locate any executables from the crash log') 556 return self.target 557 558 def get_target(self): 559 return self.target 560 561 562def usage(): 563 print("Usage: lldb-symbolicate.py [-n name] executable-image") 564 sys.exit(0) 565 566 567class Interactive(cmd.Cmd): 568 '''Interactive prompt for analyzing one or more Darwin crash logs, type "help" to see a list of supported commands.''' 569 image_option_parser = None 570 571 def __init__(self, crash_logs): 572 cmd.Cmd.__init__(self) 573 self.use_rawinput = False 574 self.intro = 'Interactive crashlogs prompt, type "help" to see a list of supported commands.' 575 self.crash_logs = crash_logs 576 self.prompt = '% ' 577 578 def default(self, line): 579 '''Catch all for unknown command, which will exit the interpreter.''' 580 print("uknown command: %s" % line) 581 return True 582 583 def do_q(self, line): 584 '''Quit command''' 585 return True 586 587 def do_quit(self, line): 588 '''Quit command''' 589 return True 590 591 def do_symbolicate(self, line): 592 description = '''Symbolicate one or more darwin crash log files by index to provide source file and line information, 593 inlined stack frames back to the concrete functions, and disassemble the location of the crash 594 for the first frame of the crashed thread.''' 595 option_parser = CreateSymbolicateCrashLogOptions( 596 'symbolicate', description, False) 597 command_args = shlex.split(line) 598 try: 599 (options, args) = option_parser.parse_args(command_args) 600 except: 601 return 602 603 if args: 604 # We have arguments, they must valid be crash log file indexes 605 for idx_str in args: 606 idx = int(idx_str) 607 if idx < len(self.crash_logs): 608 SymbolicateCrashLog(self.crash_logs[idx], options) 609 else: 610 print('error: crash log index %u is out of range' % (idx)) 611 else: 612 # No arguments, symbolicate all crash logs using the options 613 # provided 614 for idx in range(len(self.crash_logs)): 615 SymbolicateCrashLog(self.crash_logs[idx], options) 616 617 def do_list(self, line=None): 618 '''Dump a list of all crash logs that are currently loaded. 619 620 USAGE: list''' 621 print('%u crash logs are loaded:' % len(self.crash_logs)) 622 for (crash_log_idx, crash_log) in enumerate(self.crash_logs): 623 print('[%u] = %s' % (crash_log_idx, crash_log.path)) 624 625 def do_image(self, line): 626 '''Dump information about one or more binary images in the crash log given an image basename, or all images if no arguments are provided.''' 627 usage = "usage: %prog [options] <PATH> [PATH ...]" 628 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.''' 629 command_args = shlex.split(line) 630 if not self.image_option_parser: 631 self.image_option_parser = optparse.OptionParser( 632 description=description, prog='image', usage=usage) 633 self.image_option_parser.add_option( 634 '-a', 635 '--all', 636 action='store_true', 637 help='show all images', 638 default=False) 639 try: 640 (options, args) = self.image_option_parser.parse_args(command_args) 641 except: 642 return 643 644 if args: 645 for image_path in args: 646 fullpath_search = image_path[0] == '/' 647 for (crash_log_idx, crash_log) in enumerate(self.crash_logs): 648 matches_found = 0 649 for (image_idx, image) in enumerate(crash_log.images): 650 if fullpath_search: 651 if image.get_resolved_path() == image_path: 652 matches_found += 1 653 print('[%u] ' % (crash_log_idx), image) 654 else: 655 image_basename = image.get_resolved_path_basename() 656 if image_basename == image_path: 657 matches_found += 1 658 print('[%u] ' % (crash_log_idx), image) 659 if matches_found == 0: 660 for (image_idx, image) in enumerate(crash_log.images): 661 resolved_image_path = image.get_resolved_path() 662 if resolved_image_path and string.find( 663 image.get_resolved_path(), image_path) >= 0: 664 print('[%u] ' % (crash_log_idx), image) 665 else: 666 for crash_log in self.crash_logs: 667 for (image_idx, image) in enumerate(crash_log.images): 668 print('[%u] %s' % (image_idx, image)) 669 return False 670 671 672def interactive_crashlogs(options, args): 673 crash_log_files = list() 674 for arg in args: 675 for resolved_path in glob.glob(arg): 676 crash_log_files.append(resolved_path) 677 678 crash_logs = list() 679 for crash_log_file in crash_log_files: 680 # print 'crash_log_file = "%s"' % crash_log_file 681 crash_log = CrashLog(crash_log_file) 682 if crash_log.error: 683 print(crash_log.error) 684 continue 685 if options.debug: 686 crash_log.dump() 687 if not crash_log.images: 688 print('error: no images in crash log "%s"' % (crash_log)) 689 continue 690 else: 691 crash_logs.append(crash_log) 692 693 interpreter = Interactive(crash_logs) 694 # List all crash logs that were imported 695 interpreter.do_list() 696 interpreter.cmdloop() 697 698 699def save_crashlog(debugger, command, exe_ctx, result, dict): 700 usage = "usage: %prog [options] <output-path>" 701 description = '''Export the state of current target into a crashlog file''' 702 parser = optparse.OptionParser( 703 description=description, 704 prog='save_crashlog', 705 usage=usage) 706 parser.add_option( 707 '-v', 708 '--verbose', 709 action='store_true', 710 dest='verbose', 711 help='display verbose debug info', 712 default=False) 713 try: 714 (options, args) = parser.parse_args(shlex.split(command)) 715 except: 716 result.PutCString("error: invalid options") 717 return 718 if len(args) != 1: 719 result.PutCString( 720 "error: invalid arguments, a single output file is the only valid argument") 721 return 722 out_file = open(args[0], 'w') 723 if not out_file: 724 result.PutCString( 725 "error: failed to open file '%s' for writing...", 726 args[0]) 727 return 728 target = exe_ctx.target 729 if target: 730 identifier = target.executable.basename 731 process = exe_ctx.process 732 if process: 733 pid = process.id 734 if pid != lldb.LLDB_INVALID_PROCESS_ID: 735 out_file.write( 736 'Process: %s [%u]\n' % 737 (identifier, pid)) 738 out_file.write('Path: %s\n' % (target.executable.fullpath)) 739 out_file.write('Identifier: %s\n' % (identifier)) 740 out_file.write('\nDate/Time: %s\n' % 741 (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) 742 out_file.write( 743 'OS Version: Mac OS X %s (%s)\n' % 744 (platform.mac_ver()[0], subprocess.check_output('sysctl -n kern.osversion', shell=True))) 745 out_file.write('Report Version: 9\n') 746 for thread_idx in range(process.num_threads): 747 thread = process.thread[thread_idx] 748 out_file.write('\nThread %u:\n' % (thread_idx)) 749 for (frame_idx, frame) in enumerate(thread.frames): 750 frame_pc = frame.pc 751 frame_offset = 0 752 if frame.function: 753 block = frame.GetFrameBlock() 754 block_range = block.range[frame.addr] 755 if block_range: 756 block_start_addr = block_range[0] 757 frame_offset = frame_pc - block_start_addr.load_addr 758 else: 759 frame_offset = frame_pc - frame.function.addr.load_addr 760 elif frame.symbol: 761 frame_offset = frame_pc - frame.symbol.addr.load_addr 762 out_file.write( 763 '%-3u %-32s 0x%16.16x %s' % 764 (frame_idx, frame.module.file.basename, frame_pc, frame.name)) 765 if frame_offset > 0: 766 out_file.write(' + %u' % (frame_offset)) 767 line_entry = frame.line_entry 768 if line_entry: 769 if options.verbose: 770 # This will output the fullpath + line + column 771 out_file.write(' %s' % (line_entry)) 772 else: 773 out_file.write( 774 ' %s:%u' % 775 (line_entry.file.basename, line_entry.line)) 776 column = line_entry.column 777 if column: 778 out_file.write(':%u' % (column)) 779 out_file.write('\n') 780 781 out_file.write('\nBinary Images:\n') 782 for module in target.modules: 783 text_segment = module.section['__TEXT'] 784 if text_segment: 785 text_segment_load_addr = text_segment.GetLoadAddress(target) 786 if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS: 787 text_segment_end_load_addr = text_segment_load_addr + text_segment.size 788 identifier = module.file.basename 789 module_version = '???' 790 module_version_array = module.GetVersion() 791 if module_version_array: 792 module_version = '.'.join( 793 map(str, module_version_array)) 794 out_file.write( 795 ' 0x%16.16x - 0x%16.16x %s (%s - ???) <%s> %s\n' % 796 (text_segment_load_addr, 797 text_segment_end_load_addr, 798 identifier, 799 module_version, 800 module.GetUUIDString(), 801 module.file.fullpath)) 802 out_file.close() 803 else: 804 result.PutCString("error: invalid target") 805 806 807def Symbolicate(debugger, command, result, dict): 808 try: 809 SymbolicateCrashLogs(shlex.split(command)) 810 except: 811 result.PutCString("error: python exception %s" % sys.exc_info()[0]) 812 813 814def SymbolicateCrashLog(crash_log, options): 815 if crash_log.error: 816 print(crash_log.error) 817 return 818 if options.debug: 819 crash_log.dump() 820 if not crash_log.images: 821 print('error: no images in crash log') 822 return 823 824 if options.dump_image_list: 825 print("Binary Images:") 826 for image in crash_log.images: 827 if options.verbose: 828 print(image.debug_dump()) 829 else: 830 print(image) 831 832 target = crash_log.create_target() 833 if not target: 834 return 835 exe_module = target.GetModuleAtIndex(0) 836 images_to_load = list() 837 loaded_images = list() 838 if options.load_all_images: 839 # --load-all option was specified, load everything up 840 for image in crash_log.images: 841 images_to_load.append(image) 842 else: 843 # Only load the images found in stack frames for the crashed threads 844 if options.crashed_only: 845 for thread in crash_log.threads: 846 if thread.did_crash(): 847 for ident in thread.idents: 848 images = crash_log.find_images_with_identifier(ident) 849 if images: 850 for image in images: 851 images_to_load.append(image) 852 else: 853 print('error: can\'t find image for identifier "%s"' % ident) 854 else: 855 for ident in crash_log.idents: 856 images = crash_log.find_images_with_identifier(ident) 857 if images: 858 for image in images: 859 images_to_load.append(image) 860 else: 861 print('error: can\'t find image for identifier "%s"' % ident) 862 863 for image in images_to_load: 864 if image not in loaded_images: 865 err = image.add_module(target) 866 if err: 867 print(err) 868 else: 869 # print 'loaded %s' % image 870 loaded_images.append(image) 871 872 if crash_log.backtraces: 873 for thread in crash_log.backtraces: 874 thread.dump_symbolicated(crash_log, options) 875 print() 876 877 for thread in crash_log.threads: 878 thread.dump_symbolicated(crash_log, options) 879 print() 880 881 882def CreateSymbolicateCrashLogOptions( 883 command_name, 884 description, 885 add_interactive_options): 886 usage = "usage: %prog [options] <FILE> [FILE ...]" 887 option_parser = optparse.OptionParser( 888 description=description, prog='crashlog', usage=usage) 889 option_parser.add_option( 890 '--verbose', 891 '-v', 892 action='store_true', 893 dest='verbose', 894 help='display verbose debug info', 895 default=False) 896 option_parser.add_option( 897 '--debug', 898 '-g', 899 action='store_true', 900 dest='debug', 901 help='display verbose debug logging', 902 default=False) 903 option_parser.add_option( 904 '--load-all', 905 '-a', 906 action='store_true', 907 dest='load_all_images', 908 help='load all executable images, not just the images found in the crashed stack frames', 909 default=False) 910 option_parser.add_option( 911 '--images', 912 action='store_true', 913 dest='dump_image_list', 914 help='show image list', 915 default=False) 916 option_parser.add_option( 917 '--debug-delay', 918 type='int', 919 dest='debug_delay', 920 metavar='NSEC', 921 help='pause for NSEC seconds for debugger', 922 default=0) 923 option_parser.add_option( 924 '--crashed-only', 925 '-c', 926 action='store_true', 927 dest='crashed_only', 928 help='only symbolicate the crashed thread', 929 default=False) 930 option_parser.add_option( 931 '--disasm-depth', 932 '-d', 933 type='int', 934 dest='disassemble_depth', 935 help='set the depth in stack frames that should be disassembled (default is 1)', 936 default=1) 937 option_parser.add_option( 938 '--disasm-all', 939 '-D', 940 action='store_true', 941 dest='disassemble_all_threads', 942 help='enabled disassembly of frames on all threads (not just the crashed thread)', 943 default=False) 944 option_parser.add_option( 945 '--disasm-before', 946 '-B', 947 type='int', 948 dest='disassemble_before', 949 help='the number of instructions to disassemble before the frame PC', 950 default=4) 951 option_parser.add_option( 952 '--disasm-after', 953 '-A', 954 type='int', 955 dest='disassemble_after', 956 help='the number of instructions to disassemble after the frame PC', 957 default=4) 958 option_parser.add_option( 959 '--source-context', 960 '-C', 961 type='int', 962 metavar='NLINES', 963 dest='source_context', 964 help='show NLINES source lines of source context (default = 4)', 965 default=4) 966 option_parser.add_option( 967 '--source-frames', 968 type='int', 969 metavar='NFRAMES', 970 dest='source_frames', 971 help='show source for NFRAMES (default = 4)', 972 default=4) 973 option_parser.add_option( 974 '--source-all', 975 action='store_true', 976 dest='source_all', 977 help='show source for all threads, not just the crashed thread', 978 default=False) 979 if add_interactive_options: 980 option_parser.add_option( 981 '-i', 982 '--interactive', 983 action='store_true', 984 help='parse all crash logs and enter interactive mode', 985 default=False) 986 return option_parser 987 988 989def SymbolicateCrashLogs(command_args): 990 description = '''Symbolicate one or more darwin crash log files to provide source file and line information, 991inlined stack frames back to the concrete functions, and disassemble the location of the crash 992for the first frame of the crashed thread. 993If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter 994for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been 995created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows 996you to explore the program as if it were stopped at the locations described in the crash log and functions can 997be disassembled and lookups can be performed using the addresses found in the crash log.''' 998 option_parser = CreateSymbolicateCrashLogOptions( 999 'crashlog', description, True) 1000 try: 1001 (options, args) = option_parser.parse_args(command_args) 1002 except: 1003 return 1004 1005 if options.debug: 1006 print('command_args = %s' % command_args) 1007 print('options', options) 1008 print('args', args) 1009 1010 if options.debug_delay > 0: 1011 print("Waiting %u seconds for debugger to attach..." % options.debug_delay) 1012 time.sleep(options.debug_delay) 1013 error = lldb.SBError() 1014 1015 if args: 1016 if options.interactive: 1017 interactive_crashlogs(options, args) 1018 else: 1019 for crash_log_file in args: 1020 crash_log = CrashLog(crash_log_file) 1021 SymbolicateCrashLog(crash_log, options) 1022if __name__ == '__main__': 1023 # Create a new debugger instance 1024 lldb.debugger = lldb.SBDebugger.Create() 1025 SymbolicateCrashLogs(sys.argv[1:]) 1026 lldb.SBDebugger.Destroy(lldb.debugger) 1027elif getattr(lldb, 'debugger', None): 1028 lldb.debugger.HandleCommand( 1029 'command script add -f lldb.macosx.crashlog.Symbolicate crashlog') 1030 lldb.debugger.HandleCommand( 1031 'command script add -f lldb.macosx.crashlog.save_crashlog save_crashlog') 1032 print('"crashlog" and "save_crashlog" command installed, use the "--help" option for detailed help') 1033