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