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