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