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 abc 30import argparse 31import concurrent.futures 32import contextlib 33import datetime 34import json 35import os 36import platform 37import plistlib 38import re 39import shlex 40import string 41import subprocess 42import sys 43import tempfile 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 = ( 58 ["xcrun", "lldb", "-P"] if platform.system() == "Darwin" else ["lldb", "-P"] 59 ) 60 # Extend the PYTHONPATH if the path exists and isn't already there. 61 lldb_python_path = subprocess.check_output(command).decode("utf-8").strip() 62 if os.path.exists(lldb_python_path) and not sys.path.__contains__(lldb_python_path): 63 sys.path.append(lldb_python_path) 64 # Try importing LLDB again. 65 try: 66 import lldb 67 except ImportError: 68 print( 69 "error: couldn't locate the 'lldb' module, please set PYTHONPATH correctly" 70 ) 71 sys.exit(1) 72 73from lldb.utils import symbolication 74from lldb.plugins.scripted_process import INTEL64_GPR, ARM64_GPR 75 76 77def read_plist(s): 78 if sys.version_info.major == 3: 79 return plistlib.loads(s) 80 else: 81 return plistlib.readPlistFromString(s) 82 83 84class CrashLog(symbolication.Symbolicator): 85 class Thread: 86 """Class that represents a thread in a darwin crash log""" 87 88 def __init__(self, index, app_specific_backtrace, arch): 89 self.index = index 90 self.id = index 91 self.images = list() 92 self.frames = list() 93 self.idents = list() 94 self.registers = dict() 95 self.reason = None 96 self.name = None 97 self.queue = None 98 self.crashed = False 99 self.app_specific_backtrace = app_specific_backtrace 100 self.arch = arch 101 102 def dump_registers(self, prefix=""): 103 registers_info = None 104 sorted_registers = {} 105 106 def sort_dict(d): 107 sorted_keys = list(d.keys()) 108 sorted_keys.sort() 109 return {k: d[k] for k in sorted_keys} 110 111 if self.arch: 112 if "x86_64" == self.arch: 113 registers_info = INTEL64_GPR 114 elif "arm64" in self.arch: 115 registers_info = ARM64_GPR 116 else: 117 print("unknown target architecture: %s" % self.arch) 118 return 119 120 # Add registers available in the register information dictionary. 121 for reg_info in registers_info: 122 reg_name = None 123 if reg_info["name"] in self.registers: 124 reg_name = reg_info["name"] 125 elif ( 126 "generic" in reg_info and reg_info["generic"] in self.registers 127 ): 128 reg_name = reg_info["generic"] 129 else: 130 # Skip register that are present in the register information dictionary but not present in the report. 131 continue 132 133 reg_val = self.registers[reg_name] 134 sorted_registers[reg_name] = reg_val 135 136 unknown_parsed_registers = {} 137 for reg_name in self.registers: 138 if reg_name not in sorted_registers: 139 unknown_parsed_registers[reg_name] = self.registers[reg_name] 140 141 sorted_registers.update(sort_dict(unknown_parsed_registers)) 142 143 else: 144 sorted_registers = sort_dict(self.registers) 145 146 for reg_name, reg_val in sorted_registers.items(): 147 print("%s %-8s = %#16.16x" % (prefix, reg_name, reg_val)) 148 149 def dump(self, prefix=""): 150 if self.app_specific_backtrace: 151 print( 152 "%Application Specific Backtrace[%u] %s" 153 % (prefix, self.index, self.reason) 154 ) 155 else: 156 print("%sThread[%u] %s" % (prefix, self.index, self.reason)) 157 if self.frames: 158 print("%s Frames:" % (prefix)) 159 for frame in self.frames: 160 frame.dump(prefix + " ") 161 if self.registers: 162 print("%s Registers:" % (prefix)) 163 self.dump_registers(prefix) 164 165 def dump_symbolicated(self, crash_log, options): 166 this_thread_crashed = self.app_specific_backtrace 167 if not this_thread_crashed: 168 this_thread_crashed = self.did_crash() 169 if options.crashed_only and this_thread_crashed == False: 170 return 171 172 print("%s" % self) 173 display_frame_idx = -1 174 for frame_idx, frame in enumerate(self.frames): 175 disassemble = ( 176 this_thread_crashed or options.disassemble_all_threads 177 ) and frame_idx < options.disassemble_depth 178 179 # Except for the zeroth frame, we should subtract 1 from every 180 # frame pc to get the previous line entry. 181 pc = frame.pc & crash_log.addr_mask 182 pc = pc if frame_idx == 0 or pc == 0 else pc - 1 183 symbolicated_frame_addresses = crash_log.symbolicate( 184 pc, options.verbose 185 ) 186 187 if symbolicated_frame_addresses: 188 symbolicated_frame_address_idx = 0 189 for symbolicated_frame_address in symbolicated_frame_addresses: 190 display_frame_idx += 1 191 print("[%3u] %s" % (frame_idx, symbolicated_frame_address)) 192 if ( 193 (options.source_all or self.did_crash()) 194 and display_frame_idx < options.source_frames 195 and options.source_context 196 ): 197 source_context = options.source_context 198 line_entry = ( 199 symbolicated_frame_address.get_symbol_context().line_entry 200 ) 201 if line_entry.IsValid(): 202 strm = lldb.SBStream() 203 if line_entry: 204 crash_log.debugger.GetSourceManager().DisplaySourceLinesWithLineNumbers( 205 line_entry.file, 206 line_entry.line, 207 source_context, 208 source_context, 209 "->", 210 strm, 211 ) 212 source_text = strm.GetData() 213 if source_text: 214 # Indent the source a bit 215 indent_str = " " 216 join_str = "\n" + indent_str 217 print( 218 "%s%s" 219 % ( 220 indent_str, 221 join_str.join(source_text.split("\n")), 222 ) 223 ) 224 if symbolicated_frame_address_idx == 0: 225 if disassemble: 226 instructions = ( 227 symbolicated_frame_address.get_instructions() 228 ) 229 if instructions: 230 print() 231 symbolication.disassemble_instructions( 232 crash_log.get_target(), 233 instructions, 234 frame.pc, 235 options.disassemble_before, 236 options.disassemble_after, 237 frame.index > 0, 238 ) 239 print() 240 symbolicated_frame_address_idx += 1 241 else: 242 print(frame) 243 if self.registers: 244 print() 245 self.dump_registers() 246 elif self.crashed: 247 print() 248 print("No thread state (register information) available") 249 250 def add_ident(self, ident): 251 if ident not in self.idents: 252 self.idents.append(ident) 253 254 def did_crash(self): 255 return self.reason is not None 256 257 def __str__(self): 258 if self.app_specific_backtrace: 259 s = "Application Specific Backtrace[%u]" % self.index 260 else: 261 s = "Thread[%u]" % self.index 262 if self.reason: 263 s += " %s" % self.reason 264 return s 265 266 class Frame: 267 """Class that represents a stack frame in a thread in a darwin crash log""" 268 269 def __init__(self, index, pc, description): 270 self.pc = pc 271 self.description = description 272 self.index = index 273 274 def __str__(self): 275 if self.description: 276 return "[%3u] 0x%16.16x %s" % (self.index, self.pc, self.description) 277 else: 278 return "[%3u] 0x%16.16x" % (self.index, self.pc) 279 280 def dump(self, prefix): 281 print("%s%s" % (prefix, str(self))) 282 283 class DarwinImage(symbolication.Image): 284 """Class that represents a binary images in a darwin crash log""" 285 286 dsymForUUIDBinary = "/usr/local/bin/dsymForUUID" 287 if not os.path.exists(dsymForUUIDBinary): 288 try: 289 dsymForUUIDBinary = ( 290 subprocess.check_output("which dsymForUUID", shell=True) 291 .decode("utf-8") 292 .rstrip("\n") 293 ) 294 except: 295 dsymForUUIDBinary = "" 296 297 dwarfdump_uuid_regex = re.compile("UUID: ([-0-9a-fA-F]+) \(([^\(]+)\) .*") 298 299 def __init__( 300 self, text_addr_lo, text_addr_hi, identifier, version, uuid, path, verbose 301 ): 302 symbolication.Image.__init__(self, path, uuid) 303 self.add_section( 304 symbolication.Section(text_addr_lo, text_addr_hi, "__TEXT") 305 ) 306 self.identifier = identifier 307 self.version = version 308 self.verbose = verbose 309 310 def show_symbol_progress(self): 311 """ 312 Hide progress output and errors from system frameworks as they are plentiful. 313 """ 314 if self.verbose: 315 return True 316 return not ( 317 self.path.startswith("/System/Library/") 318 or self.path.startswith("/usr/lib/") 319 ) 320 321 def find_matching_slice(self): 322 dwarfdump_cmd_output = subprocess.check_output( 323 'dwarfdump --uuid "%s"' % self.path, shell=True 324 ).decode("utf-8") 325 self_uuid = self.get_uuid() 326 for line in dwarfdump_cmd_output.splitlines(): 327 match = self.dwarfdump_uuid_regex.search(line) 328 if match: 329 dwarf_uuid_str = match.group(1) 330 dwarf_uuid = uuid.UUID(dwarf_uuid_str) 331 if self_uuid == dwarf_uuid: 332 self.resolved_path = self.path 333 self.arch = match.group(2) 334 return True 335 if not self.resolved_path: 336 self.unavailable = True 337 if self.show_symbol_progress(): 338 print( 339 ( 340 "error\n error: unable to locate '%s' with UUID %s" 341 % (self.path, self.get_normalized_uuid_string()) 342 ) 343 ) 344 return False 345 346 def locate_module_and_debug_symbols(self): 347 # Don't load a module twice... 348 if self.resolved: 349 return True 350 # Mark this as resolved so we don't keep trying 351 self.resolved = True 352 uuid_str = self.get_normalized_uuid_string() 353 if self.show_symbol_progress(): 354 with print_lock: 355 print("Getting symbols for %s %s..." % (uuid_str, self.path)) 356 # Keep track of unresolved source paths. 357 unavailable_source_paths = set() 358 if os.path.exists(self.dsymForUUIDBinary): 359 dsym_for_uuid_command = "%s %s" % (self.dsymForUUIDBinary, uuid_str) 360 s = subprocess.check_output(dsym_for_uuid_command, shell=True) 361 if s: 362 try: 363 plist_root = read_plist(s) 364 except: 365 with print_lock: 366 print( 367 ( 368 "Got exception: ", 369 sys.exc_info()[1], 370 " handling dsymForUUID output: \n", 371 s, 372 ) 373 ) 374 raise 375 if plist_root: 376 plist = plist_root[uuid_str] 377 if plist: 378 if "DBGArchitecture" in plist: 379 self.arch = plist["DBGArchitecture"] 380 if "DBGDSYMPath" in plist: 381 self.symfile = os.path.realpath(plist["DBGDSYMPath"]) 382 if "DBGSymbolRichExecutable" in plist: 383 self.path = os.path.expanduser( 384 plist["DBGSymbolRichExecutable"] 385 ) 386 self.resolved_path = self.path 387 if "DBGSourcePathRemapping" in plist: 388 path_remapping = plist["DBGSourcePathRemapping"] 389 for _, value in path_remapping.items(): 390 source_path = os.path.expanduser(value) 391 if not os.path.exists(source_path): 392 unavailable_source_paths.add(source_path) 393 if not self.resolved_path and os.path.exists(self.path): 394 if not self.find_matching_slice(): 395 return False 396 if not self.resolved_path and not os.path.exists(self.path): 397 try: 398 mdfind_results = ( 399 subprocess.check_output( 400 [ 401 "/usr/bin/mdfind", 402 "com_apple_xcode_dsym_uuids == %s" % uuid_str, 403 ] 404 ) 405 .decode("utf-8") 406 .splitlines() 407 ) 408 found_matching_slice = False 409 for dsym in mdfind_results: 410 dwarf_dir = os.path.join(dsym, "Contents/Resources/DWARF") 411 if not os.path.exists(dwarf_dir): 412 # Not a dSYM bundle, probably an Xcode archive. 413 continue 414 with print_lock: 415 print('falling back to binary inside "%s"' % dsym) 416 self.symfile = dsym 417 for filename in os.listdir(dwarf_dir): 418 self.path = os.path.join(dwarf_dir, filename) 419 if self.find_matching_slice(): 420 found_matching_slice = True 421 break 422 if found_matching_slice: 423 break 424 except: 425 pass 426 if (self.resolved_path and os.path.exists(self.resolved_path)) or ( 427 self.path and os.path.exists(self.path) 428 ): 429 with print_lock: 430 print("Resolved symbols for %s %s..." % (uuid_str, self.path)) 431 if len(unavailable_source_paths): 432 for source_path in unavailable_source_paths: 433 print( 434 "Could not access remapped source path for %s %s" 435 % (uuid_str, source_path) 436 ) 437 return True 438 else: 439 self.unavailable = True 440 return False 441 442 def __init__(self, debugger, path, verbose): 443 """CrashLog constructor that take a path to a darwin crash log file""" 444 symbolication.Symbolicator.__init__(self, debugger) 445 self.path = os.path.expanduser(path) 446 self.info_lines = list() 447 self.system_profile = list() 448 self.threads = list() 449 self.backtraces = list() # For application specific backtraces 450 self.idents = ( 451 list() 452 ) # A list of the required identifiers for doing all stack backtraces 453 self.errors = list() 454 self.exception = dict() 455 self.crashed_thread_idx = -1 456 self.version = -1 457 self.target = None 458 self.verbose = verbose 459 self.process_id = None 460 self.process_identifier = None 461 self.process_path = None 462 self.process_arch = None 463 464 def dump(self): 465 print("Crash Log File: %s" % (self.path)) 466 if self.backtraces: 467 print("\nApplication Specific Backtraces:") 468 for thread in self.backtraces: 469 thread.dump(" ") 470 print("\nThreads:") 471 for thread in self.threads: 472 thread.dump(" ") 473 print("\nImages:") 474 for image in self.images: 475 image.dump(" ") 476 477 def set_main_image(self, identifier): 478 for i, image in enumerate(self.images): 479 if image.identifier == identifier: 480 self.images.insert(0, self.images.pop(i)) 481 break 482 483 def find_image_with_identifier(self, identifier): 484 for image in self.images: 485 if image.identifier == identifier: 486 return image 487 regex_text = "^.*\.%s$" % (re.escape(identifier)) 488 regex = re.compile(regex_text) 489 for image in self.images: 490 if regex.match(image.identifier): 491 return image 492 return None 493 494 def create_target(self): 495 if self.target is None: 496 self.target = symbolication.Symbolicator.create_target(self) 497 if self.target: 498 return self.target 499 # We weren't able to open the main executable as, but we can still 500 # symbolicate 501 print("crashlog.create_target()...2") 502 if self.idents: 503 for ident in self.idents: 504 image = self.find_image_with_identifier(ident) 505 if image: 506 self.target = image.create_target(self.debugger) 507 if self.target: 508 return self.target # success 509 print("crashlog.create_target()...3") 510 for image in self.images: 511 self.target = image.create_target(self.debugger) 512 if self.target: 513 return self.target # success 514 print("crashlog.create_target()...4") 515 print("error: Unable to locate any executables from the crash log.") 516 print(" Try loading the executable into lldb before running crashlog") 517 print( 518 " and/or make sure the .dSYM bundles can be found by Spotlight." 519 ) 520 return self.target 521 522 def get_target(self): 523 return self.target 524 525 526class CrashLogFormatException(Exception): 527 pass 528 529 530class CrashLogParseException(Exception): 531 pass 532 533 534class InteractiveCrashLogException(Exception): 535 pass 536 537 538class CrashLogParser: 539 @staticmethod 540 def create(debugger, path, options): 541 data = JSONCrashLogParser.is_valid_json(path) 542 if data: 543 parser = JSONCrashLogParser(debugger, path, options) 544 parser.data = data 545 return parser 546 else: 547 return TextCrashLogParser(debugger, path, options) 548 549 def __init__(self, debugger, path, options): 550 self.path = os.path.expanduser(path) 551 self.options = options 552 self.crashlog = CrashLog(debugger, self.path, self.options.verbose) 553 554 @abc.abstractmethod 555 def parse(self): 556 pass 557 558 559class JSONCrashLogParser(CrashLogParser): 560 @staticmethod 561 def is_valid_json(path): 562 def parse_json(buffer): 563 try: 564 return json.loads(buffer) 565 except: 566 # The first line can contain meta data. Try stripping it and 567 # try again. 568 head, _, tail = buffer.partition("\n") 569 return json.loads(tail) 570 571 with open(path, "r", encoding="utf-8") as f: 572 buffer = f.read() 573 try: 574 return parse_json(buffer) 575 except: 576 return None 577 578 def __init__(self, debugger, path, options): 579 super().__init__(debugger, path, options) 580 581 def parse(self): 582 try: 583 self.parse_process_info(self.data) 584 self.parse_images(self.data["usedImages"]) 585 self.parse_main_image(self.data) 586 self.parse_threads(self.data["threads"]) 587 if "asi" in self.data: 588 self.crashlog.asi = self.data["asi"] 589 if "asiBacktraces" in self.data: 590 self.parse_app_specific_backtraces(self.data["asiBacktraces"]) 591 if "lastExceptionBacktrace" in self.data: 592 self.crashlog.asb = self.data["lastExceptionBacktrace"] 593 self.parse_errors(self.data) 594 thread = self.crashlog.threads[self.crashlog.crashed_thread_idx] 595 reason = self.parse_crash_reason(self.data["exception"]) 596 if thread.reason: 597 thread.reason = "{} {}".format(thread.reason, reason) 598 else: 599 thread.reason = reason 600 except (KeyError, ValueError, TypeError) as e: 601 raise CrashLogParseException( 602 "Failed to parse JSON crashlog: {}: {}".format(type(e).__name__, e) 603 ) 604 605 return self.crashlog 606 607 def get_used_image(self, idx): 608 return self.data["usedImages"][idx] 609 610 def parse_process_info(self, json_data): 611 self.crashlog.process_id = json_data["pid"] 612 self.crashlog.process_identifier = json_data["procName"] 613 if "procPath" in json_data: 614 self.crashlog.process_path = json_data["procPath"] 615 616 def parse_crash_reason(self, json_exception): 617 self.crashlog.exception = json_exception 618 exception_type = json_exception["type"] 619 exception_signal = " " 620 if "signal" in json_exception: 621 exception_signal += "({})".format(json_exception["signal"]) 622 623 if "codes" in json_exception: 624 exception_extra = " ({})".format(json_exception["codes"]) 625 elif "subtype" in json_exception: 626 exception_extra = " ({})".format(json_exception["subtype"]) 627 else: 628 exception_extra = "" 629 return "{}{}{}".format(exception_type, exception_signal, exception_extra) 630 631 def parse_images(self, json_images): 632 for json_image in json_images: 633 img_uuid = uuid.UUID(json_image["uuid"]) 634 low = int(json_image["base"]) 635 high = low + int(json_image["size"]) if "size" in json_image else low 636 name = json_image["name"] if "name" in json_image else "" 637 path = json_image["path"] if "path" in json_image else "" 638 version = "" 639 darwin_image = self.crashlog.DarwinImage( 640 low, high, name, version, img_uuid, path, self.options.verbose 641 ) 642 if "arch" in json_image: 643 darwin_image.arch = json_image["arch"] 644 if path == self.crashlog.process_path: 645 self.crashlog.process_arch = darwin_image.arch 646 self.crashlog.images.append(darwin_image) 647 648 def parse_main_image(self, json_data): 649 if "procName" in json_data: 650 proc_name = json_data["procName"] 651 self.crashlog.set_main_image(proc_name) 652 653 def parse_frames(self, thread, json_frames): 654 idx = 0 655 for json_frame in json_frames: 656 image_id = int(json_frame["imageIndex"]) 657 json_image = self.get_used_image(image_id) 658 ident = json_image["name"] if "name" in json_image else "" 659 thread.add_ident(ident) 660 if ident not in self.crashlog.idents: 661 self.crashlog.idents.append(ident) 662 663 frame_offset = int(json_frame["imageOffset"]) 664 image_addr = self.get_used_image(image_id)["base"] 665 pc = image_addr + frame_offset 666 667 if "symbol" in json_frame: 668 symbol = json_frame["symbol"] 669 location = 0 670 if "symbolLocation" in json_frame and json_frame["symbolLocation"]: 671 location = int(json_frame["symbolLocation"]) 672 image = self.crashlog.images[image_id] 673 image.symbols[symbol] = { 674 "name": symbol, 675 "type": "code", 676 "address": frame_offset - location, 677 } 678 679 thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset)) 680 681 # on arm64 systems, if it jump through a null function pointer, 682 # we end up at address 0 and the crash reporter unwinder 683 # misses the frame that actually faulted. 684 # But $lr can tell us where the last BL/BLR instruction used 685 # was at, so insert that address as the caller stack frame. 686 if idx == 0 and pc == 0 and "lr" in thread.registers: 687 pc = thread.registers["lr"] 688 for image in self.data["usedImages"]: 689 text_lo = image["base"] 690 text_hi = text_lo + image["size"] 691 if text_lo <= pc < text_hi: 692 idx += 1 693 frame_offset = pc - text_lo 694 thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset)) 695 break 696 697 idx += 1 698 699 def parse_threads(self, json_threads): 700 idx = 0 701 for json_thread in json_threads: 702 thread = self.crashlog.Thread(idx, False, self.crashlog.process_arch) 703 if "name" in json_thread: 704 thread.name = json_thread["name"] 705 thread.reason = json_thread["name"] 706 if "id" in json_thread: 707 thread.id = int(json_thread["id"]) 708 if json_thread.get("triggered", False): 709 self.crashlog.crashed_thread_idx = idx 710 thread.crashed = True 711 if "threadState" in json_thread: 712 thread.registers = self.parse_thread_registers( 713 json_thread["threadState"] 714 ) 715 if "queue" in json_thread: 716 thread.queue = json_thread.get("queue") 717 self.parse_frames(thread, json_thread.get("frames", [])) 718 self.crashlog.threads.append(thread) 719 idx += 1 720 721 def parse_asi_backtrace(self, thread, bt): 722 for line in bt.split("\n"): 723 frame_match = TextCrashLogParser.frame_regex.search(line) 724 if not frame_match: 725 print("error: can't parse application specific backtrace.") 726 return False 727 728 frame_id = ( 729 frame_img_name 730 ) = ( 731 frame_addr 732 ) = ( 733 frame_symbol 734 ) = frame_offset = frame_file = frame_line = frame_column = None 735 736 if len(frame_match.groups()) == 3: 737 # Get the image UUID from the frame image name. 738 (frame_id, frame_img_name, frame_addr) = frame_match.groups() 739 elif len(frame_match.groups()) == 5: 740 ( 741 frame_id, 742 frame_img_name, 743 frame_addr, 744 frame_symbol, 745 frame_offset, 746 ) = frame_match.groups() 747 elif len(frame_match.groups()) == 7: 748 ( 749 frame_id, 750 frame_img_name, 751 frame_addr, 752 frame_symbol, 753 frame_offset, 754 frame_file, 755 frame_line, 756 ) = frame_match.groups() 757 elif len(frame_match.groups()) == 8: 758 ( 759 frame_id, 760 frame_img_name, 761 frame_addr, 762 frame_symbol, 763 frame_offset, 764 frame_file, 765 frame_line, 766 frame_column, 767 ) = frame_match.groups() 768 769 thread.add_ident(frame_img_name) 770 if frame_img_name not in self.crashlog.idents: 771 self.crashlog.idents.append(frame_img_name) 772 773 description = "" 774 if frame_img_name and frame_addr and frame_symbol: 775 description = frame_symbol 776 frame_offset_value = 0 777 if frame_offset: 778 description += " + " + frame_offset 779 frame_offset_value = int(frame_offset, 0) 780 for image in self.crashlog.images: 781 if image.identifier == frame_img_name: 782 image.symbols[frame_symbol] = { 783 "name": frame_symbol, 784 "type": "code", 785 "address": int(frame_addr, 0) - frame_offset_value, 786 } 787 788 thread.frames.append( 789 self.crashlog.Frame(int(frame_id), int(frame_addr, 0), description) 790 ) 791 792 return True 793 794 def parse_app_specific_backtraces(self, json_app_specific_bts): 795 for idx, backtrace in enumerate(json_app_specific_bts): 796 thread = self.crashlog.Thread(idx, True, self.crashlog.process_arch) 797 thread.queue = "Application Specific Backtrace" 798 if self.parse_asi_backtrace(thread, backtrace): 799 self.crashlog.threads.append(thread) 800 801 def parse_thread_registers(self, json_thread_state, prefix=None): 802 registers = dict() 803 for key, state in json_thread_state.items(): 804 if key == "rosetta": 805 registers.update(self.parse_thread_registers(state)) 806 continue 807 if key == "x": 808 gpr_dict = {str(idx): reg for idx, reg in enumerate(state)} 809 registers.update(self.parse_thread_registers(gpr_dict, key)) 810 continue 811 if key == "flavor": 812 if not self.crashlog.process_arch: 813 if state == "ARM_THREAD_STATE64": 814 self.crashlog.process_arch = "arm64" 815 elif state == "X86_THREAD_STATE": 816 self.crashlog.process_arch = "x86_64" 817 continue 818 try: 819 value = int(state["value"]) 820 registers["{}{}".format(prefix or "", key)] = value 821 except (KeyError, ValueError, TypeError): 822 pass 823 return registers 824 825 def parse_errors(self, json_data): 826 if "reportNotes" in json_data: 827 self.crashlog.errors = json_data["reportNotes"] 828 829 830class TextCrashLogParser(CrashLogParser): 831 parent_process_regex = re.compile(r"^Parent Process:\s*(.*)\[(\d+)\]") 832 thread_state_regex = re.compile(r"^Thread \d+ crashed with") 833 thread_instrs_regex = re.compile(r"^Thread \d+ instruction stream") 834 thread_regex = re.compile(r"^Thread (\d+).*:") 835 app_backtrace_regex = re.compile(r"^Application Specific Backtrace (\d+).*:") 836 837 class VersionRegex: 838 version = r"\(.+\)|(?:arm|x86_)[0-9a-z]+" 839 840 class FrameRegex(VersionRegex): 841 @classmethod 842 def get(cls): 843 index = r"^(\d+)\s+" 844 img_name = r"(.+?)\s+" 845 version = r"(?:" + super().version + r"\s+)?" 846 address = r"(0x[0-9a-fA-F]{4,})" # 4 digits or more 847 848 symbol = """ 849 (?: 850 [ ]+ 851 (?P<symbol>.+) 852 (?: 853 [ ]\+[ ] 854 (?P<symbol_offset>\d+) 855 ) 856 (?: 857 [ ]\( 858 (?P<file_name>[^:]+):(?P<line_number>\d+) 859 (?: 860 :(?P<column_num>\d+) 861 )? 862 )? 863 )? 864 """ 865 866 return re.compile( 867 index + img_name + version + address + symbol, flags=re.VERBOSE 868 ) 869 870 frame_regex = FrameRegex.get() 871 null_frame_regex = re.compile(r"^\d+\s+\?\?\?\s+0{4,} +") 872 image_regex_uuid = re.compile( 873 r"(0x[0-9a-fA-F]+)" # img_lo 874 r"\s+-\s+" # - 875 r"(0x[0-9a-fA-F]+)\s+" # img_hi 876 r"[+]?(.+?)\s+" # img_name 877 r"(?:(" + VersionRegex.version + r")\s+)?" # img_version 878 r"(?:<([-0-9a-fA-F]+)>\s+)?" # img_uuid 879 r"(\?+|/.*)" # img_path 880 ) 881 exception_type_regex = re.compile( 882 r"^Exception Type:\s+(EXC_[A-Z_]+)(?:\s+\((.*)\))?" 883 ) 884 exception_codes_regex = re.compile( 885 r"^Exception Codes:\s+(0x[0-9a-fA-F]+),\s*(0x[0-9a-fA-F]+)" 886 ) 887 exception_extra_regex = re.compile(r"^Exception\s+.*:\s+(.*)") 888 889 class CrashLogParseMode: 890 NORMAL = 0 891 THREAD = 1 892 IMAGES = 2 893 THREGS = 3 894 SYSTEM = 4 895 INSTRS = 5 896 897 def __init__(self, debugger, path, options): 898 super().__init__(debugger, path, options) 899 self.thread = None 900 self.app_specific_backtrace = False 901 self.parse_mode = self.CrashLogParseMode.NORMAL 902 self.parsers = { 903 self.CrashLogParseMode.NORMAL: self.parse_normal, 904 self.CrashLogParseMode.THREAD: self.parse_thread, 905 self.CrashLogParseMode.IMAGES: self.parse_images, 906 self.CrashLogParseMode.THREGS: self.parse_thread_registers, 907 self.CrashLogParseMode.SYSTEM: self.parse_system, 908 self.CrashLogParseMode.INSTRS: self.parse_instructions, 909 } 910 self.symbols = {} 911 912 def parse(self): 913 with open(self.path, "r", encoding="utf-8") as f: 914 lines = f.read().splitlines() 915 916 idx = 0 917 lines_count = len(lines) 918 while True: 919 if idx >= lines_count: 920 break 921 922 line = lines[idx] 923 line_len = len(line) 924 925 if line_len == 0: 926 if self.thread: 927 if self.parse_mode == self.CrashLogParseMode.THREAD: 928 if self.thread.index == self.crashlog.crashed_thread_idx: 929 self.thread.reason = "" 930 if hasattr(self.crashlog, "thread_exception"): 931 self.thread.reason += self.crashlog.thread_exception 932 if hasattr(self.crashlog, "thread_exception_data"): 933 self.thread.reason += ( 934 " (%s)" % self.crashlog.thread_exception_data 935 ) 936 if self.app_specific_backtrace: 937 self.crashlog.backtraces.append(self.thread) 938 else: 939 self.crashlog.threads.append(self.thread) 940 self.thread = None 941 942 empty_lines = 1 943 while ( 944 idx + empty_lines < lines_count 945 and len(lines[idx + empty_lines]) == 0 946 ): 947 empty_lines = empty_lines + 1 948 949 if ( 950 empty_lines == 1 951 and idx + empty_lines < lines_count - 1 952 and self.parse_mode != self.CrashLogParseMode.NORMAL 953 ): 954 # check if next line can be parsed with the current parse mode 955 next_line_idx = idx + empty_lines 956 if self.parsers[self.parse_mode](lines[next_line_idx]): 957 # If that suceeded, skip the empty line and the next line. 958 idx = next_line_idx + 1 959 continue 960 self.parse_mode = self.CrashLogParseMode.NORMAL 961 962 self.parsers[self.parse_mode](line) 963 964 idx = idx + 1 965 966 return self.crashlog 967 968 def parse_exception(self, line): 969 if not line.startswith("Exception"): 970 return False 971 if line.startswith("Exception Type:"): 972 self.crashlog.thread_exception = line[15:].strip() 973 exception_type_match = self.exception_type_regex.search(line) 974 if exception_type_match: 975 exc_type, exc_signal = exception_type_match.groups() 976 self.crashlog.exception["type"] = exc_type 977 if exc_signal: 978 self.crashlog.exception["signal"] = exc_signal 979 elif line.startswith("Exception Subtype:"): 980 self.crashlog.thread_exception_subtype = line[18:].strip() 981 if "type" in self.crashlog.exception: 982 self.crashlog.exception[ 983 "subtype" 984 ] = self.crashlog.thread_exception_subtype 985 elif line.startswith("Exception Codes:"): 986 self.crashlog.thread_exception_data = line[16:].strip() 987 if "type" not in self.crashlog.exception: 988 return False 989 exception_codes_match = self.exception_codes_regex.search(line) 990 if exception_codes_match: 991 self.crashlog.exception["codes"] = self.crashlog.thread_exception_data 992 code, subcode = exception_codes_match.groups() 993 self.crashlog.exception["rawCodes"] = [ 994 int(code, base=16), 995 int(subcode, base=16), 996 ] 997 else: 998 if "type" not in self.crashlog.exception: 999 return False 1000 exception_extra_match = self.exception_extra_regex.search(line) 1001 if exception_extra_match: 1002 self.crashlog.exception["message"] = exception_extra_match.group(1) 1003 return True 1004 1005 def parse_normal(self, line): 1006 if line.startswith("Process:"): 1007 (self.crashlog.process_name, pid_with_brackets) = ( 1008 line[8:].strip().split(" [") 1009 ) 1010 self.crashlog.process_id = pid_with_brackets.strip("[]") 1011 elif line.startswith("Path:"): 1012 self.crashlog.process_path = line[5:].strip() 1013 elif line.startswith("Identifier:"): 1014 self.crashlog.process_identifier = line[11:].strip() 1015 elif line.startswith("Version:"): 1016 version_string = line[8:].strip() 1017 matched_pair = re.search("(.+)\((.+)\)", version_string) 1018 if matched_pair: 1019 self.crashlog.process_version = matched_pair.group(1) 1020 self.crashlog.process_compatability_version = matched_pair.group(2) 1021 else: 1022 self.crashlog.process = version_string 1023 self.crashlog.process_compatability_version = version_string 1024 elif line.startswith("Code Type:"): 1025 if "ARM-64" in line: 1026 self.crashlog.process_arch = "arm64" 1027 elif "X86-64" in line: 1028 self.crashlog.process_arch = "x86_64" 1029 elif self.parent_process_regex.search(line): 1030 parent_process_match = self.parent_process_regex.search(line) 1031 self.crashlog.parent_process_name = parent_process_match.group(1) 1032 self.crashlog.parent_process_id = parent_process_match.group(2) 1033 elif line.startswith("Exception"): 1034 self.parse_exception(line) 1035 return 1036 elif line.startswith("Crashed Thread:"): 1037 self.crashlog.crashed_thread_idx = int(line[15:].strip().split()[0]) 1038 return 1039 elif line.startswith("Triggered by Thread:"): # iOS 1040 self.crashlog.crashed_thread_idx = int(line[20:].strip().split()[0]) 1041 return 1042 elif line.startswith("Report Version:"): 1043 self.crashlog.version = int(line[15:].strip()) 1044 return 1045 elif line.startswith("System Profile:"): 1046 self.parse_mode = self.CrashLogParseMode.SYSTEM 1047 return 1048 elif ( 1049 line.startswith("Interval Since Last Report:") 1050 or line.startswith("Crashes Since Last Report:") 1051 or line.startswith("Per-App Interval Since Last Report:") 1052 or line.startswith("Per-App Crashes Since Last Report:") 1053 or line.startswith("Sleep/Wake UUID:") 1054 or line.startswith("Anonymous UUID:") 1055 ): 1056 # ignore these 1057 return 1058 elif line.startswith("Thread"): 1059 thread_state_match = self.thread_state_regex.search(line) 1060 if thread_state_match: 1061 self.app_specific_backtrace = False 1062 thread_state_match = self.thread_regex.search(line) 1063 thread_idx = int(thread_state_match.group(1)) 1064 self.parse_mode = self.CrashLogParseMode.THREGS 1065 self.thread = self.crashlog.threads[thread_idx] 1066 return 1067 thread_insts_match = self.thread_instrs_regex.search(line) 1068 if thread_insts_match: 1069 self.parse_mode = self.CrashLogParseMode.INSTRS 1070 return 1071 thread_match = self.thread_regex.search(line) 1072 if thread_match: 1073 self.app_specific_backtrace = False 1074 self.parse_mode = self.CrashLogParseMode.THREAD 1075 thread_idx = int(thread_match.group(1)) 1076 self.thread = self.crashlog.Thread( 1077 thread_idx, False, self.crashlog.process_arch 1078 ) 1079 return 1080 return 1081 elif line.startswith("Binary Images:"): 1082 self.parse_mode = self.CrashLogParseMode.IMAGES 1083 return 1084 elif line.startswith("Application Specific Backtrace"): 1085 app_backtrace_match = self.app_backtrace_regex.search(line) 1086 if app_backtrace_match: 1087 self.parse_mode = self.CrashLogParseMode.THREAD 1088 self.app_specific_backtrace = True 1089 idx = int(app_backtrace_match.group(1)) 1090 self.thread = self.crashlog.Thread( 1091 idx, True, self.crashlog.process_arch 1092 ) 1093 elif line.startswith("Last Exception Backtrace:"): # iOS 1094 self.parse_mode = self.CrashLogParseMode.THREAD 1095 self.app_specific_backtrace = True 1096 idx = 1 1097 self.thread = self.crashlog.Thread(idx, True, self.crashlog.process_arch) 1098 self.crashlog.info_lines.append(line.strip()) 1099 1100 def parse_thread(self, line): 1101 if line.startswith("Thread"): 1102 return False 1103 if self.null_frame_regex.search(line): 1104 print('warning: thread parser ignored null-frame: "%s"' % line) 1105 return False 1106 frame_match = self.frame_regex.search(line) 1107 if not frame_match: 1108 print('error: frame regex failed for line: "%s"' % line) 1109 return False 1110 1111 frame_id = ( 1112 frame_img_name 1113 ) = ( 1114 frame_addr 1115 ) = frame_symbol = frame_offset = frame_file = frame_line = frame_column = None 1116 1117 if len(frame_match.groups()) == 3: 1118 # Get the image UUID from the frame image name. 1119 (frame_id, frame_img_name, frame_addr) = frame_match.groups() 1120 elif len(frame_match.groups()) == 5: 1121 ( 1122 frame_id, 1123 frame_img_name, 1124 frame_addr, 1125 frame_symbol, 1126 frame_offset, 1127 ) = frame_match.groups() 1128 elif len(frame_match.groups()) == 7: 1129 ( 1130 frame_id, 1131 frame_img_name, 1132 frame_addr, 1133 frame_symbol, 1134 frame_offset, 1135 frame_file, 1136 frame_line, 1137 ) = frame_match.groups() 1138 elif len(frame_match.groups()) == 8: 1139 ( 1140 frame_id, 1141 frame_img_name, 1142 frame_addr, 1143 frame_symbol, 1144 frame_offset, 1145 frame_file, 1146 frame_line, 1147 frame_column, 1148 ) = frame_match.groups() 1149 1150 self.thread.add_ident(frame_img_name) 1151 if frame_img_name not in self.crashlog.idents: 1152 self.crashlog.idents.append(frame_img_name) 1153 1154 description = "" 1155 # Since images are parsed after threads, we need to build a 1156 # map for every image with a list of all the symbols and addresses 1157 if frame_img_name and frame_addr and frame_symbol: 1158 description = frame_symbol 1159 frame_offset_value = 0 1160 if frame_offset: 1161 description += " + " + frame_offset 1162 frame_offset_value = int(frame_offset, 0) 1163 if frame_img_name not in self.symbols: 1164 self.symbols[frame_img_name] = list() 1165 self.symbols[frame_img_name].append( 1166 { 1167 "name": frame_symbol, 1168 "address": int(frame_addr, 0) - frame_offset_value, 1169 } 1170 ) 1171 1172 self.thread.frames.append( 1173 self.crashlog.Frame(int(frame_id), int(frame_addr, 0), description) 1174 ) 1175 1176 return True 1177 1178 def parse_images(self, line): 1179 image_match = self.image_regex_uuid.search(line) 1180 if image_match: 1181 ( 1182 img_lo, 1183 img_hi, 1184 img_name, 1185 img_version, 1186 img_uuid, 1187 img_path, 1188 ) = image_match.groups() 1189 1190 image = self.crashlog.DarwinImage( 1191 int(img_lo, 0), 1192 int(img_hi, 0), 1193 img_name.strip(), 1194 img_version.strip() if img_version else "", 1195 uuid.UUID(img_uuid), 1196 img_path, 1197 self.options.verbose, 1198 ) 1199 unqualified_img_name = os.path.basename(img_path) 1200 if unqualified_img_name in self.symbols: 1201 for symbol in self.symbols[unqualified_img_name]: 1202 image.symbols[symbol["name"]] = { 1203 "name": symbol["name"], 1204 "type": "code", 1205 # NOTE: "address" is actually the symbol image offset 1206 "address": symbol["address"] - int(img_lo, 0), 1207 } 1208 1209 self.crashlog.images.append(image) 1210 return True 1211 else: 1212 if self.options.debug: 1213 print("error: image regex failed for: %s" % line) 1214 return False 1215 1216 def parse_thread_registers(self, line): 1217 # "r12: 0x00007fff6b5939c8 r13: 0x0000000007000006 r14: 0x0000000000002a03 r15: 0x0000000000000c00" 1218 reg_values = re.findall("([a-z0-9]+): (0x[0-9a-f]+)", line, re.I) 1219 for reg, value in reg_values: 1220 self.thread.registers[reg] = int(value, 16) 1221 return len(reg_values) != 0 1222 1223 def parse_system(self, line): 1224 self.crashlog.system_profile.append(line) 1225 return True 1226 1227 def parse_instructions(self, line): 1228 pass 1229 1230 1231def save_crashlog(debugger, command, exe_ctx, result, dict): 1232 usage = "save_crashlog [options] <output-path>" 1233 description = """Export the state of current target into a crashlog file""" 1234 parser = argparse.ArgumentParser( 1235 description=description, 1236 prog="save_crashlog", 1237 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 1238 ) 1239 parser.add_argument( 1240 "output", 1241 metavar="output-file", 1242 type=argparse.FileType("w", encoding="utf-8"), 1243 nargs=1, 1244 ) 1245 parser.add_argument( 1246 "-v", 1247 "--verbose", 1248 action="store_true", 1249 dest="verbose", 1250 help="display verbose debug info", 1251 default=False, 1252 ) 1253 try: 1254 options = parser.parse_args(shlex.split(command)) 1255 except Exception as e: 1256 result.SetError(str(e)) 1257 return 1258 target = exe_ctx.target 1259 if target: 1260 out_file = options.output 1261 identifier = target.executable.basename 1262 process = exe_ctx.process 1263 if process: 1264 pid = process.id 1265 if pid != lldb.LLDB_INVALID_PROCESS_ID: 1266 out_file.write("Process: %s [%u]\n" % (identifier, pid)) 1267 out_file.write("Path: %s\n" % (target.executable.fullpath)) 1268 out_file.write("Identifier: %s\n" % (identifier)) 1269 out_file.write( 1270 "\nDate/Time: %s\n" 1271 % (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) 1272 ) 1273 out_file.write( 1274 "OS Version: Mac OS X %s (%s)\n" 1275 % ( 1276 platform.mac_ver()[0], 1277 subprocess.check_output("sysctl -n kern.osversion", shell=True).decode( 1278 "utf-8" 1279 ), 1280 ) 1281 ) 1282 out_file.write("Report Version: 9\n") 1283 for thread_idx in range(process.num_threads): 1284 thread = process.thread[thread_idx] 1285 out_file.write("\nThread %u:\n" % (thread_idx)) 1286 for frame_idx, frame in enumerate(thread.frames): 1287 frame_pc = frame.pc 1288 frame_offset = 0 1289 if frame.function: 1290 block = frame.GetFrameBlock() 1291 block_range = block.range[frame.addr] 1292 if block_range: 1293 block_start_addr = block_range[0] 1294 frame_offset = frame_pc - block_start_addr.GetLoadAddress( 1295 target 1296 ) 1297 else: 1298 frame_offset = frame_pc - frame.function.addr.GetLoadAddress( 1299 target 1300 ) 1301 elif frame.symbol: 1302 frame_offset = frame_pc - frame.symbol.addr.GetLoadAddress(target) 1303 out_file.write( 1304 "%-3u %-32s 0x%16.16x %s" 1305 % (frame_idx, frame.module.file.basename, frame_pc, frame.name) 1306 ) 1307 if frame_offset > 0: 1308 out_file.write(" + %u" % (frame_offset)) 1309 line_entry = frame.line_entry 1310 if line_entry: 1311 if options.verbose: 1312 # This will output the fullpath + line + column 1313 out_file.write(" %s" % (line_entry)) 1314 else: 1315 out_file.write( 1316 " %s:%u" % (line_entry.file.basename, line_entry.line) 1317 ) 1318 column = line_entry.column 1319 if column: 1320 out_file.write(":%u" % (column)) 1321 out_file.write("\n") 1322 1323 out_file.write("\nBinary Images:\n") 1324 for module in target.modules: 1325 text_segment = module.section["__TEXT"] 1326 if text_segment: 1327 text_segment_load_addr = text_segment.GetLoadAddress(target) 1328 if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS: 1329 text_segment_end_load_addr = ( 1330 text_segment_load_addr + text_segment.size 1331 ) 1332 identifier = module.file.basename 1333 module_version = "???" 1334 module_version_array = module.GetVersion() 1335 if module_version_array: 1336 module_version = ".".join(map(str, module_version_array)) 1337 out_file.write( 1338 " 0x%16.16x - 0x%16.16x %s (%s - ???) <%s> %s\n" 1339 % ( 1340 text_segment_load_addr, 1341 text_segment_end_load_addr, 1342 identifier, 1343 module_version, 1344 module.GetUUIDString(), 1345 module.file.fullpath, 1346 ) 1347 ) 1348 out_file.close() 1349 else: 1350 result.SetError("invalid target") 1351 1352 1353class Symbolicate: 1354 def __init__(self, debugger, internal_dict): 1355 pass 1356 1357 def __call__(self, debugger, command, exe_ctx, result): 1358 SymbolicateCrashLogs(debugger, shlex.split(command), result, True) 1359 1360 def get_short_help(self): 1361 return "Symbolicate one or more darwin crash log files." 1362 1363 def get_long_help(self): 1364 arg_parser = CrashLogOptionParser() 1365 return arg_parser.format_help() 1366 1367 1368def SymbolicateCrashLog(crash_log, options): 1369 if options.debug: 1370 crash_log.dump() 1371 if not crash_log.images: 1372 print("error: no images in crash log") 1373 return 1374 1375 if options.dump_image_list: 1376 print("Binary Images:") 1377 for image in crash_log.images: 1378 if options.verbose: 1379 print(image.debug_dump()) 1380 else: 1381 print(image) 1382 1383 target = crash_log.create_target() 1384 if not target: 1385 return 1386 1387 if options.load_all_images: 1388 for image in crash_log.images: 1389 image.resolve = True 1390 elif options.crashed_only: 1391 for thread in crash_log.threads: 1392 if thread.did_crash(): 1393 for ident in thread.idents: 1394 for image in crash_log.find_images_with_identifier(ident): 1395 image.resolve = True 1396 1397 futures = [] 1398 loaded_images = [] 1399 with tempfile.TemporaryDirectory() as obj_dir: 1400 with concurrent.futures.ThreadPoolExecutor() as executor: 1401 1402 def add_module(image, target, obj_dir): 1403 return image, image.add_module(target, obj_dir) 1404 1405 for image in crash_log.images: 1406 futures.append( 1407 executor.submit( 1408 add_module, image=image, target=target, obj_dir=obj_dir 1409 ) 1410 ) 1411 for future in concurrent.futures.as_completed(futures): 1412 image, err = future.result() 1413 if err: 1414 print(err) 1415 else: 1416 loaded_images.append(image) 1417 1418 if crash_log.backtraces: 1419 for thread in crash_log.backtraces: 1420 thread.dump_symbolicated(crash_log, options) 1421 print() 1422 1423 for thread in crash_log.threads: 1424 thread.dump_symbolicated(crash_log, options) 1425 print() 1426 1427 if crash_log.errors: 1428 print("Errors:") 1429 for error in crash_log.errors: 1430 print(error) 1431 1432 1433def load_crashlog_in_scripted_process(debugger, crashlog_path, options, result): 1434 crashlog = CrashLogParser.create(debugger, crashlog_path, options).parse() 1435 1436 target = lldb.SBTarget() 1437 # 1. Try to use the user-provided target 1438 if options.target_path: 1439 target = debugger.CreateTarget(options.target_path) 1440 if not target: 1441 raise InteractiveCrashLogException( 1442 "couldn't create target provided by the user (%s)" % options.target_path 1443 ) 1444 1445 # 2. If the user didn't provide a target, try to create a target using the symbolicator 1446 if not target or not target.IsValid(): 1447 target = crashlog.create_target() 1448 # 3. If that didn't work, create a dummy target 1449 if target is None or not target.IsValid(): 1450 arch = crashlog.process_arch 1451 if not arch: 1452 raise InteractiveCrashLogException( 1453 "couldn't create find the architecture to create the target" 1454 ) 1455 target = debugger.CreateTargetWithFileAndArch(None, arch) 1456 # 4. Fail 1457 if target is None or not target.IsValid(): 1458 raise InteractiveCrashLogException("couldn't create target") 1459 1460 ci = debugger.GetCommandInterpreter() 1461 if not ci: 1462 raise InteractiveCrashLogException("couldn't get command interpreter") 1463 1464 ci.HandleCommand("script from lldb.macosx import crashlog_scripted_process", result) 1465 if not result.Succeeded(): 1466 raise InteractiveCrashLogException( 1467 "couldn't import crashlog scripted process module" 1468 ) 1469 1470 structured_data = lldb.SBStructuredData() 1471 structured_data.SetFromJSON( 1472 json.dumps( 1473 {"file_path": crashlog_path, "load_all_images": options.load_all_images} 1474 ) 1475 ) 1476 launch_info = lldb.SBLaunchInfo(None) 1477 launch_info.SetProcessPluginName("ScriptedProcess") 1478 launch_info.SetScriptedProcessClassName( 1479 "crashlog_scripted_process.CrashLogScriptedProcess" 1480 ) 1481 launch_info.SetScriptedProcessDictionary(structured_data) 1482 launch_info.SetLaunchFlags(lldb.eLaunchFlagStopAtEntry) 1483 1484 error = lldb.SBError() 1485 process = target.Launch(launch_info, error) 1486 1487 if not process or error.Fail(): 1488 raise InteractiveCrashLogException("couldn't launch Scripted Process", error) 1489 1490 process.GetScriptedImplementation().set_crashlog(crashlog) 1491 process.Continue() 1492 1493 if not options.skip_status: 1494 1495 @contextlib.contextmanager 1496 def synchronous(debugger): 1497 async_state = debugger.GetAsync() 1498 debugger.SetAsync(False) 1499 try: 1500 yield 1501 finally: 1502 debugger.SetAsync(async_state) 1503 1504 with synchronous(debugger): 1505 run_options = lldb.SBCommandInterpreterRunOptions() 1506 run_options.SetStopOnError(True) 1507 run_options.SetStopOnCrash(True) 1508 run_options.SetEchoCommands(True) 1509 1510 commands_stream = lldb.SBStream() 1511 commands_stream.Print("process status --verbose\n") 1512 commands_stream.Print("thread backtrace --extended true\n") 1513 error = debugger.SetInputString(commands_stream.GetData()) 1514 if error.Success(): 1515 debugger.RunCommandInterpreter(True, False, run_options, 0, False, True) 1516 1517 1518def CreateSymbolicateCrashLogOptions( 1519 command_name, description, add_interactive_options 1520): 1521 usage = "crashlog [options] <FILE> [FILE ...]" 1522 arg_parser = argparse.ArgumentParser( 1523 description=description, 1524 prog="crashlog", 1525 usage=usage, 1526 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 1527 ) 1528 arg_parser.add_argument( 1529 "reports", 1530 metavar="FILE", 1531 type=str, 1532 nargs="*", 1533 help="crash report(s) to symbolicate", 1534 ) 1535 1536 arg_parser.add_argument( 1537 "--version", 1538 "-V", 1539 dest="version", 1540 action="store_true", 1541 help="Show crashlog version", 1542 default=False, 1543 ) 1544 arg_parser.add_argument( 1545 "--verbose", 1546 "-v", 1547 action="store_true", 1548 dest="verbose", 1549 help="display verbose debug info", 1550 default=False, 1551 ) 1552 arg_parser.add_argument( 1553 "--debug", 1554 "-g", 1555 action="store_true", 1556 dest="debug", 1557 help="display verbose debug logging", 1558 default=False, 1559 ) 1560 arg_parser.add_argument( 1561 "--load-all", 1562 "-a", 1563 action="store_true", 1564 dest="load_all_images", 1565 help="load all executable images, not just the images found in the " 1566 "crashed stack frames, loads stackframes for all the threads in " 1567 "interactive mode.", 1568 default=False, 1569 ) 1570 arg_parser.add_argument( 1571 "--images", 1572 action="store_true", 1573 dest="dump_image_list", 1574 help="show image list", 1575 default=False, 1576 ) 1577 arg_parser.add_argument( 1578 "--debug-delay", 1579 type=int, 1580 dest="debug_delay", 1581 metavar="NSEC", 1582 help="pause for NSEC seconds for debugger", 1583 default=0, 1584 ) 1585 arg_parser.add_argument( 1586 "--crashed-only", 1587 "-c", 1588 action=argparse.BooleanOptionalAction, 1589 dest="crashed_only", 1590 help="only symbolicate the crashed thread", 1591 default=True, 1592 ) 1593 arg_parser.add_argument( 1594 "--disasm-depth", 1595 "-d", 1596 type=int, 1597 dest="disassemble_depth", 1598 help="set the depth in stack frames that should be disassembled", 1599 default=1, 1600 ) 1601 arg_parser.add_argument( 1602 "--disasm-all", 1603 "-D", 1604 action="store_true", 1605 dest="disassemble_all_threads", 1606 help="enabled disassembly of frames on all threads (not just the crashed thread)", 1607 default=False, 1608 ) 1609 arg_parser.add_argument( 1610 "--disasm-before", 1611 "-B", 1612 type=int, 1613 dest="disassemble_before", 1614 help="the number of instructions to disassemble before the frame PC", 1615 default=4, 1616 ) 1617 arg_parser.add_argument( 1618 "--disasm-after", 1619 "-A", 1620 type=int, 1621 dest="disassemble_after", 1622 help="the number of instructions to disassemble after the frame PC", 1623 default=4, 1624 ) 1625 arg_parser.add_argument( 1626 "--source-context", 1627 "-C", 1628 type=int, 1629 metavar="NLINES", 1630 dest="source_context", 1631 help="show NLINES source lines of source context", 1632 default=4, 1633 ) 1634 arg_parser.add_argument( 1635 "--source-frames", 1636 type=int, 1637 metavar="NFRAMES", 1638 dest="source_frames", 1639 help="show source for NFRAMES", 1640 default=4, 1641 ) 1642 arg_parser.add_argument( 1643 "--source-all", 1644 action="store_true", 1645 dest="source_all", 1646 help="show source for all threads, not just the crashed thread", 1647 default=False, 1648 ) 1649 if add_interactive_options: 1650 arg_parser.add_argument( 1651 "-i", 1652 "--interactive", 1653 action="store_true", 1654 help="parse a crash log and load it in a ScriptedProcess", 1655 default=False, 1656 ) 1657 arg_parser.add_argument( 1658 "-b", 1659 "--batch", 1660 action="store_true", 1661 help="dump symbolicated stackframes without creating a debug session", 1662 default=True, 1663 ) 1664 arg_parser.add_argument( 1665 "--target", 1666 "-t", 1667 dest="target_path", 1668 help="the target binary path that should be used for interactive crashlog (optional)", 1669 default=None, 1670 ) 1671 arg_parser.add_argument( 1672 "--skip-status", 1673 "-s", 1674 dest="skip_status", 1675 action="store_true", 1676 help="prevent the interactive crashlog to dump the process status and thread backtrace at launch", 1677 default=False, 1678 ) 1679 return arg_parser 1680 1681 1682def CrashLogOptionParser(): 1683 description = """Symbolicate one or more darwin crash log files to provide source file and line information, 1684inlined stack frames back to the concrete functions, and disassemble the location of the crash 1685for the first frame of the crashed thread. 1686If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter 1687for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been 1688created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows 1689you to explore the program as if it were stopped at the locations described in the crash log and functions can 1690be disassembled and lookups can be performed using the addresses found in the crash log.""" 1691 return CreateSymbolicateCrashLogOptions("crashlog", description, True) 1692 1693 1694def SymbolicateCrashLogs(debugger, command_args, result, is_command): 1695 arg_parser = CrashLogOptionParser() 1696 1697 if not len(command_args): 1698 arg_parser.print_help() 1699 return 1700 1701 try: 1702 options = arg_parser.parse_args(command_args) 1703 except Exception as e: 1704 result.SetError(str(e)) 1705 return 1706 1707 # Interactive mode requires running the crashlog command from inside lldb. 1708 if options.interactive and not is_command: 1709 lldb_exec = ( 1710 subprocess.check_output(["/usr/bin/xcrun", "-f", "lldb"]) 1711 .decode("utf-8") 1712 .strip() 1713 ) 1714 sys.exit( 1715 os.execv( 1716 lldb_exec, 1717 [ 1718 lldb_exec, 1719 "-o", 1720 "command script import lldb.macosx", 1721 "-o", 1722 "crashlog {}".format(shlex.join(command_args)), 1723 ], 1724 ) 1725 ) 1726 1727 if options.version: 1728 print(debugger.GetVersionString()) 1729 return 1730 1731 if options.debug: 1732 print("command_args = %s" % command_args) 1733 print("options", options) 1734 print("args", options.reports) 1735 1736 if options.debug_delay > 0: 1737 print("Waiting %u seconds for debugger to attach..." % options.debug_delay) 1738 time.sleep(options.debug_delay) 1739 error = lldb.SBError() 1740 1741 def should_run_in_interactive_mode(options, ci): 1742 if options.interactive: 1743 return True 1744 elif options.batch: 1745 return False 1746 # elif ci and ci.IsInteractive(): 1747 # return True 1748 else: 1749 return False 1750 1751 ci = debugger.GetCommandInterpreter() 1752 1753 if options.reports: 1754 for crashlog_file in options.reports: 1755 crashlog_path = os.path.expanduser(crashlog_file) 1756 if not os.path.exists(crashlog_path): 1757 raise FileNotFoundError( 1758 "crashlog file %s does not exist" % crashlog_path 1759 ) 1760 if should_run_in_interactive_mode(options, ci): 1761 try: 1762 load_crashlog_in_scripted_process( 1763 debugger, crashlog_path, options, result 1764 ) 1765 except InteractiveCrashLogException as e: 1766 result.SetError(str(e)) 1767 else: 1768 crash_log = CrashLogParser.create( 1769 debugger, crashlog_path, options 1770 ).parse() 1771 SymbolicateCrashLog(crash_log, options) 1772 1773 1774if __name__ == "__main__": 1775 # Create a new debugger instance 1776 debugger = lldb.SBDebugger.Create() 1777 result = lldb.SBCommandReturnObject() 1778 SymbolicateCrashLogs(debugger, sys.argv[1:], result, False) 1779 lldb.SBDebugger.Destroy(debugger) 1780 1781 1782def __lldb_init_module(debugger, internal_dict): 1783 debugger.HandleCommand( 1784 "command script add -o -c lldb.macosx.crashlog.Symbolicate -C disk-file crashlog" 1785 ) 1786 debugger.HandleCommand( 1787 "command script add -o -f lldb.macosx.crashlog.save_crashlog -C disk-file save_crashlog" 1788 ) 1789 print( 1790 '"crashlog" and "save_crashlog" commands have been installed, use ' 1791 'the "--help" options on these commands for detailed help.' 1792 ) 1793