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