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