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