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 concurrent.futures 31import contextlib 32import datetime 33import json 34import optparse 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, verbose): 541 data = JSONCrashLogParser.is_valid_json(path) 542 if data: 543 parser = JSONCrashLogParser(debugger, path, verbose) 544 parser.data = data 545 return parser 546 else: 547 return TextCrashLogParser(debugger, path, verbose) 548 549 def __init__(self, debugger, path, verbose): 550 self.path = os.path.expanduser(path) 551 self.verbose = verbose 552 # List of DarwinImages sorted by their index. 553 self.images = list() 554 self.crashlog = CrashLog(debugger, self.path, self.verbose) 555 556 @abc.abstractmethod 557 def parse(self): 558 pass 559 560 561class JSONCrashLogParser(CrashLogParser): 562 @staticmethod 563 def is_valid_json(path): 564 def parse_json(buffer): 565 try: 566 return json.loads(buffer) 567 except: 568 # The first line can contain meta data. Try stripping it and 569 # try again. 570 head, _, tail = buffer.partition("\n") 571 return json.loads(tail) 572 573 with open(path, "r", encoding="utf-8") as f: 574 buffer = f.read() 575 try: 576 return parse_json(buffer) 577 except: 578 return None 579 580 def __init__(self, debugger, path, verbose): 581 super().__init__(debugger, path, verbose) 582 583 def parse(self): 584 try: 585 self.parse_process_info(self.data) 586 self.parse_images(self.data["usedImages"]) 587 self.parse_main_image(self.data) 588 self.parse_threads(self.data["threads"]) 589 if "asi" in self.data: 590 self.crashlog.asi = self.data["asi"] 591 if "asiBacktraces" in self.data: 592 self.parse_app_specific_backtraces(self.data["asiBacktraces"]) 593 if "lastExceptionBacktrace" in self.data: 594 self.crashlog.asb = self.data["lastExceptionBacktrace"] 595 self.parse_errors(self.data) 596 thread = self.crashlog.threads[self.crashlog.crashed_thread_idx] 597 reason = self.parse_crash_reason(self.data["exception"]) 598 if thread.reason: 599 thread.reason = "{} {}".format(thread.reason, reason) 600 else: 601 thread.reason = reason 602 except (KeyError, ValueError, TypeError) as e: 603 raise CrashLogParseException( 604 "Failed to parse JSON crashlog: {}: {}".format(type(e).__name__, e) 605 ) 606 607 return self.crashlog 608 609 def get_used_image(self, idx): 610 return self.data["usedImages"][idx] 611 612 def parse_process_info(self, json_data): 613 self.crashlog.process_id = json_data["pid"] 614 self.crashlog.process_identifier = json_data["procName"] 615 if "procPath" in json_data: 616 self.crashlog.process_path = json_data["procPath"] 617 618 def parse_crash_reason(self, json_exception): 619 self.crashlog.exception = json_exception 620 exception_type = json_exception["type"] 621 exception_signal = " " 622 if "signal" in json_exception: 623 exception_signal += "({})".format(json_exception["signal"]) 624 625 if "codes" in json_exception: 626 exception_extra = " ({})".format(json_exception["codes"]) 627 elif "subtype" in json_exception: 628 exception_extra = " ({})".format(json_exception["subtype"]) 629 else: 630 exception_extra = "" 631 return "{}{}{}".format(exception_type, exception_signal, exception_extra) 632 633 def parse_images(self, json_images): 634 for json_image in json_images: 635 img_uuid = uuid.UUID(json_image["uuid"]) 636 low = int(json_image["base"]) 637 high = low + int(json_image["size"]) if "size" in json_image else low 638 name = json_image["name"] if "name" in json_image else "" 639 path = json_image["path"] if "path" in json_image else "" 640 version = "" 641 darwin_image = self.crashlog.DarwinImage( 642 low, high, name, version, img_uuid, path, self.verbose 643 ) 644 if "arch" in json_image: 645 darwin_image.arch = json_image["arch"] 646 if path == self.crashlog.process_path: 647 self.crashlog.process_arch = darwin_image.arch 648 self.images.append(darwin_image) 649 self.crashlog.images.append(darwin_image) 650 651 def parse_main_image(self, json_data): 652 if "procName" in json_data: 653 proc_name = json_data["procName"] 654 self.crashlog.set_main_image(proc_name) 655 656 def parse_frames(self, thread, json_frames): 657 idx = 0 658 for json_frame in json_frames: 659 image_id = int(json_frame["imageIndex"]) 660 json_image = self.get_used_image(image_id) 661 ident = json_image["name"] if "name" in json_image else "" 662 thread.add_ident(ident) 663 if ident not in self.crashlog.idents: 664 self.crashlog.idents.append(ident) 665 666 frame_offset = int(json_frame["imageOffset"]) 667 image_addr = self.get_used_image(image_id)["base"] 668 pc = image_addr + frame_offset 669 670 if "symbol" in json_frame: 671 symbol = json_frame["symbol"] 672 location = 0 673 if "symbolLocation" in json_frame and json_frame["symbolLocation"]: 674 location = int(json_frame["symbolLocation"]) 675 image = self.images[image_id] 676 image.symbols[symbol] = { 677 "name": symbol, 678 "type": "code", 679 "address": frame_offset - location, 680 } 681 682 thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset)) 683 684 # on arm64 systems, if it jump through a null function pointer, 685 # we end up at address 0 and the crash reporter unwinder 686 # misses the frame that actually faulted. 687 # But $lr can tell us where the last BL/BLR instruction used 688 # was at, so insert that address as the caller stack frame. 689 if idx == 0 and pc == 0 and "lr" in thread.registers: 690 pc = thread.registers["lr"] 691 for image in self.data["usedImages"]: 692 text_lo = image["base"] 693 text_hi = text_lo + image["size"] 694 if text_lo <= pc < text_hi: 695 idx += 1 696 frame_offset = pc - text_lo 697 thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset)) 698 break 699 700 idx += 1 701 702 def parse_threads(self, json_threads): 703 idx = 0 704 for json_thread in json_threads: 705 thread = self.crashlog.Thread(idx, False, self.crashlog.process_arch) 706 if "name" in json_thread: 707 thread.name = json_thread["name"] 708 thread.reason = json_thread["name"] 709 if "id" in json_thread: 710 thread.id = int(json_thread["id"]) 711 if json_thread.get("triggered", False): 712 self.crashlog.crashed_thread_idx = idx 713 thread.crashed = True 714 if "threadState" in json_thread: 715 thread.registers = self.parse_thread_registers( 716 json_thread["threadState"] 717 ) 718 if "queue" in json_thread: 719 thread.queue = json_thread.get("queue") 720 self.parse_frames(thread, json_thread.get("frames", [])) 721 self.crashlog.threads.append(thread) 722 idx += 1 723 724 def parse_asi_backtrace(self, thread, bt): 725 for line in bt.split("\n"): 726 frame_match = TextCrashLogParser.frame_regex.search(line) 727 if not frame_match: 728 print("error: can't parse application specific backtrace.") 729 return False 730 731 frame_id = ( 732 frame_img_name 733 ) = ( 734 frame_addr 735 ) = ( 736 frame_symbol 737 ) = frame_offset = frame_file = frame_line = frame_column = None 738 739 if len(frame_match.groups()) == 3: 740 # Get the image UUID from the frame image name. 741 (frame_id, frame_img_name, frame_addr) = frame_match.groups() 742 elif len(frame_match.groups()) == 5: 743 ( 744 frame_id, 745 frame_img_name, 746 frame_addr, 747 frame_symbol, 748 frame_offset, 749 ) = frame_match.groups() 750 elif len(frame_match.groups()) == 7: 751 ( 752 frame_id, 753 frame_img_name, 754 frame_addr, 755 frame_symbol, 756 frame_offset, 757 frame_file, 758 frame_line, 759 ) = frame_match.groups() 760 elif len(frame_match.groups()) == 8: 761 ( 762 frame_id, 763 frame_img_name, 764 frame_addr, 765 frame_symbol, 766 frame_offset, 767 frame_file, 768 frame_line, 769 frame_column, 770 ) = frame_match.groups() 771 772 thread.add_ident(frame_img_name) 773 if frame_img_name not in self.crashlog.idents: 774 self.crashlog.idents.append(frame_img_name) 775 776 description = "" 777 if frame_img_name and frame_addr and frame_symbol: 778 description = frame_symbol 779 frame_offset_value = 0 780 if frame_offset: 781 description += " + " + frame_offset 782 frame_offset_value = int(frame_offset, 0) 783 for image in self.images: 784 if image.identifier == frame_img_name: 785 image.symbols[frame_symbol] = { 786 "name": frame_symbol, 787 "type": "code", 788 "address": int(frame_addr, 0) - frame_offset_value, 789 } 790 791 thread.frames.append( 792 self.crashlog.Frame(int(frame_id), int(frame_addr, 0), description) 793 ) 794 795 return True 796 797 def parse_app_specific_backtraces(self, json_app_specific_bts): 798 for idx, backtrace in enumerate(json_app_specific_bts): 799 thread = self.crashlog.Thread(idx, True, self.crashlog.process_arch) 800 thread.queue = "Application Specific Backtrace" 801 if self.parse_asi_backtrace(thread, backtrace): 802 self.crashlog.threads.append(thread) 803 804 def parse_thread_registers(self, json_thread_state, prefix=None): 805 registers = dict() 806 for key, state in json_thread_state.items(): 807 if key == "rosetta": 808 registers.update(self.parse_thread_registers(state)) 809 continue 810 if key == "x": 811 gpr_dict = {str(idx): reg for idx, reg in enumerate(state)} 812 registers.update(self.parse_thread_registers(gpr_dict, key)) 813 continue 814 if key == "flavor": 815 if not self.crashlog.process_arch: 816 if state == "ARM_THREAD_STATE64": 817 self.crashlog.process_arch = "arm64" 818 elif state == "X86_THREAD_STATE": 819 self.crashlog.process_arch = "x86_64" 820 continue 821 try: 822 value = int(state["value"]) 823 registers["{}{}".format(prefix or "", key)] = value 824 except (KeyError, ValueError, TypeError): 825 pass 826 return registers 827 828 def parse_errors(self, json_data): 829 if "reportNotes" in json_data: 830 self.crashlog.errors = json_data["reportNotes"] 831 832 833class CrashLogParseMode: 834 NORMAL = 0 835 THREAD = 1 836 IMAGES = 2 837 THREGS = 3 838 SYSTEM = 4 839 INSTRS = 5 840 841 842class TextCrashLogParser(CrashLogParser): 843 parent_process_regex = re.compile(r"^Parent Process:\s*(.*)\[(\d+)\]") 844 thread_state_regex = re.compile(r"^Thread \d+ crashed with") 845 thread_instrs_regex = re.compile(r"^Thread \d+ instruction stream") 846 thread_regex = re.compile(r"^Thread (\d+).*:") 847 app_backtrace_regex = re.compile(r"^Application Specific Backtrace (\d+).*:") 848 849 class VersionRegex: 850 version = r"\(.+\)|(?:arm|x86_)[0-9a-z]+" 851 852 class FrameRegex(VersionRegex): 853 @classmethod 854 def get(cls): 855 index = r"^(\d+)\s+" 856 img_name = r"(.+?)\s+" 857 version = r"(?:" + super().version + r"\s+)?" 858 address = r"(0x[0-9a-fA-F]{4,})" # 4 digits or more 859 860 symbol = """ 861 (?: 862 [ ]+ 863 (?P<symbol>.+) 864 (?: 865 [ ]\+[ ] 866 (?P<symbol_offset>\d+) 867 ) 868 (?: 869 [ ]\( 870 (?P<file_name>[^:]+):(?P<line_number>\d+) 871 (?: 872 :(?P<column_num>\d+) 873 )? 874 )? 875 )? 876 """ 877 878 return re.compile( 879 index + img_name + version + address + symbol, flags=re.VERBOSE 880 ) 881 882 frame_regex = FrameRegex.get() 883 null_frame_regex = re.compile(r"^\d+\s+\?\?\?\s+0{4,} +") 884 image_regex_uuid = re.compile( 885 r"(0x[0-9a-fA-F]+)" # img_lo 886 r"\s+-\s+" # - 887 r"(0x[0-9a-fA-F]+)\s+" # img_hi 888 r"[+]?(.+?)\s+" # img_name 889 r"(?:(" + VersionRegex.version + r")\s+)?" # img_version 890 r"(?:<([-0-9a-fA-F]+)>\s+)?" # img_uuid 891 r"(\?+|/.*)" # img_path 892 ) 893 exception_type_regex = re.compile( 894 r"^Exception Type:\s+(EXC_[A-Z_]+)(?:\s+\((.*)\))?" 895 ) 896 exception_codes_regex = re.compile( 897 r"^Exception Codes:\s+(0x[0-9a-fA-F]+),\s*(0x[0-9a-fA-F]+)" 898 ) 899 exception_extra_regex = re.compile(r"^Exception\s+.*:\s+(.*)") 900 901 def __init__(self, debugger, path, verbose): 902 super().__init__(debugger, path, verbose) 903 self.thread = None 904 self.app_specific_backtrace = False 905 self.parse_mode = CrashLogParseMode.NORMAL 906 self.parsers = { 907 CrashLogParseMode.NORMAL: self.parse_normal, 908 CrashLogParseMode.THREAD: self.parse_thread, 909 CrashLogParseMode.IMAGES: self.parse_images, 910 CrashLogParseMode.THREGS: self.parse_thread_registers, 911 CrashLogParseMode.SYSTEM: self.parse_system, 912 CrashLogParseMode.INSTRS: self.parse_instructions, 913 } 914 self.symbols = {} 915 916 def parse(self): 917 with open(self.path, "r", encoding="utf-8") as f: 918 lines = f.read().splitlines() 919 920 for line in lines: 921 line_len = len(line) 922 if line_len == 0: 923 if self.thread: 924 if self.parse_mode == CrashLogParseMode.THREAD: 925 if self.thread.index == self.crashlog.crashed_thread_idx: 926 self.thread.reason = "" 927 if hasattr(self.crashlog, "thread_exception"): 928 self.thread.reason += self.crashlog.thread_exception 929 if hasattr(self.crashlog, "thread_exception_data"): 930 self.thread.reason += ( 931 " (%s)" % self.crashlog.thread_exception_data 932 ) 933 if self.app_specific_backtrace: 934 self.crashlog.backtraces.append(self.thread) 935 else: 936 self.crashlog.threads.append(self.thread) 937 self.thread = None 938 else: 939 # only append an extra empty line if the previous line 940 # in the info_lines wasn't empty 941 if len(self.crashlog.info_lines) > 0 and len( 942 self.crashlog.info_lines[-1] 943 ): 944 self.crashlog.info_lines.append(line) 945 self.parse_mode = CrashLogParseMode.NORMAL 946 else: 947 self.parsers[self.parse_mode](line) 948 949 return self.crashlog 950 951 def parse_exception(self, line): 952 if not line.startswith("Exception"): 953 return 954 if line.startswith("Exception Type:"): 955 self.crashlog.thread_exception = line[15:].strip() 956 exception_type_match = self.exception_type_regex.search(line) 957 if exception_type_match: 958 exc_type, exc_signal = exception_type_match.groups() 959 self.crashlog.exception["type"] = exc_type 960 if exc_signal: 961 self.crashlog.exception["signal"] = exc_signal 962 elif line.startswith("Exception Subtype:"): 963 self.crashlog.thread_exception_subtype = line[18:].strip() 964 if "type" in self.crashlog.exception: 965 self.crashlog.exception[ 966 "subtype" 967 ] = self.crashlog.thread_exception_subtype 968 elif line.startswith("Exception Codes:"): 969 self.crashlog.thread_exception_data = line[16:].strip() 970 if "type" not in self.crashlog.exception: 971 return 972 exception_codes_match = self.exception_codes_regex.search(line) 973 if exception_codes_match: 974 self.crashlog.exception["codes"] = self.crashlog.thread_exception_data 975 code, subcode = exception_codes_match.groups() 976 self.crashlog.exception["rawCodes"] = [ 977 int(code, base=16), 978 int(subcode, base=16), 979 ] 980 else: 981 if "type" not in self.crashlog.exception: 982 return 983 exception_extra_match = self.exception_extra_regex.search(line) 984 if exception_extra_match: 985 self.crashlog.exception["message"] = exception_extra_match.group(1) 986 987 def parse_normal(self, line): 988 if line.startswith("Process:"): 989 (self.crashlog.process_name, pid_with_brackets) = ( 990 line[8:].strip().split(" [") 991 ) 992 self.crashlog.process_id = pid_with_brackets.strip("[]") 993 elif line.startswith("Path:"): 994 self.crashlog.process_path = line[5:].strip() 995 elif line.startswith("Identifier:"): 996 self.crashlog.process_identifier = line[11:].strip() 997 elif line.startswith("Version:"): 998 version_string = line[8:].strip() 999 matched_pair = re.search("(.+)\((.+)\)", version_string) 1000 if matched_pair: 1001 self.crashlog.process_version = matched_pair.group(1) 1002 self.crashlog.process_compatability_version = matched_pair.group(2) 1003 else: 1004 self.crashlog.process = version_string 1005 self.crashlog.process_compatability_version = version_string 1006 elif line.startswith("Code Type:"): 1007 if "ARM-64" in line: 1008 self.crashlog.process_arch = "arm64" 1009 elif "X86-64" in line: 1010 self.crashlog.process_arch = "x86_64" 1011 elif self.parent_process_regex.search(line): 1012 parent_process_match = self.parent_process_regex.search(line) 1013 self.crashlog.parent_process_name = parent_process_match.group(1) 1014 self.crashlog.parent_process_id = parent_process_match.group(2) 1015 elif line.startswith("Exception"): 1016 self.parse_exception(line) 1017 return 1018 elif line.startswith("Crashed Thread:"): 1019 self.crashlog.crashed_thread_idx = int(line[15:].strip().split()[0]) 1020 return 1021 elif line.startswith("Triggered by Thread:"): # iOS 1022 self.crashlog.crashed_thread_idx = int(line[20:].strip().split()[0]) 1023 return 1024 elif line.startswith("Report Version:"): 1025 self.crashlog.version = int(line[15:].strip()) 1026 return 1027 elif line.startswith("System Profile:"): 1028 self.parse_mode = CrashLogParseMode.SYSTEM 1029 return 1030 elif ( 1031 line.startswith("Interval Since Last Report:") 1032 or line.startswith("Crashes Since Last Report:") 1033 or line.startswith("Per-App Interval Since Last Report:") 1034 or line.startswith("Per-App Crashes Since Last Report:") 1035 or line.startswith("Sleep/Wake UUID:") 1036 or line.startswith("Anonymous UUID:") 1037 ): 1038 # ignore these 1039 return 1040 elif line.startswith("Thread"): 1041 thread_state_match = self.thread_state_regex.search(line) 1042 if thread_state_match: 1043 self.app_specific_backtrace = False 1044 thread_state_match = self.thread_regex.search(line) 1045 thread_idx = int(thread_state_match.group(1)) 1046 self.parse_mode = CrashLogParseMode.THREGS 1047 self.thread = self.crashlog.threads[thread_idx] 1048 return 1049 thread_insts_match = self.thread_instrs_regex.search(line) 1050 if thread_insts_match: 1051 self.parse_mode = CrashLogParseMode.INSTRS 1052 return 1053 thread_match = self.thread_regex.search(line) 1054 if thread_match: 1055 self.app_specific_backtrace = False 1056 self.parse_mode = CrashLogParseMode.THREAD 1057 thread_idx = int(thread_match.group(1)) 1058 self.thread = self.crashlog.Thread( 1059 thread_idx, False, self.crashlog.process_arch 1060 ) 1061 return 1062 return 1063 elif line.startswith("Binary Images:"): 1064 self.parse_mode = CrashLogParseMode.IMAGES 1065 return 1066 elif line.startswith("Application Specific Backtrace"): 1067 app_backtrace_match = self.app_backtrace_regex.search(line) 1068 if app_backtrace_match: 1069 self.parse_mode = CrashLogParseMode.THREAD 1070 self.app_specific_backtrace = True 1071 idx = int(app_backtrace_match.group(1)) 1072 self.thread = self.crashlog.Thread( 1073 idx, True, self.crashlog.process_arch 1074 ) 1075 elif line.startswith("Last Exception Backtrace:"): # iOS 1076 self.parse_mode = CrashLogParseMode.THREAD 1077 self.app_specific_backtrace = True 1078 idx = 1 1079 self.thread = self.crashlog.Thread(idx, True, self.crashlog.process_arch) 1080 self.crashlog.info_lines.append(line.strip()) 1081 1082 def parse_thread(self, line): 1083 if line.startswith("Thread"): 1084 return 1085 if self.null_frame_regex.search(line): 1086 print('warning: thread parser ignored null-frame: "%s"' % line) 1087 return 1088 frame_match = self.frame_regex.search(line) 1089 if not frame_match: 1090 print('error: frame regex failed for line: "%s"' % line) 1091 return 1092 1093 frame_id = ( 1094 frame_img_name 1095 ) = ( 1096 frame_addr 1097 ) = frame_symbol = frame_offset = frame_file = frame_line = frame_column = None 1098 1099 if len(frame_match.groups()) == 3: 1100 # Get the image UUID from the frame image name. 1101 (frame_id, frame_img_name, frame_addr) = frame_match.groups() 1102 elif len(frame_match.groups()) == 5: 1103 ( 1104 frame_id, 1105 frame_img_name, 1106 frame_addr, 1107 frame_symbol, 1108 frame_offset, 1109 ) = frame_match.groups() 1110 elif len(frame_match.groups()) == 7: 1111 ( 1112 frame_id, 1113 frame_img_name, 1114 frame_addr, 1115 frame_symbol, 1116 frame_offset, 1117 frame_file, 1118 frame_line, 1119 ) = frame_match.groups() 1120 elif len(frame_match.groups()) == 8: 1121 ( 1122 frame_id, 1123 frame_img_name, 1124 frame_addr, 1125 frame_symbol, 1126 frame_offset, 1127 frame_file, 1128 frame_line, 1129 frame_column, 1130 ) = frame_match.groups() 1131 1132 self.thread.add_ident(frame_img_name) 1133 if frame_img_name not in self.crashlog.idents: 1134 self.crashlog.idents.append(frame_img_name) 1135 1136 description = "" 1137 # Since images are parsed after threads, we need to build a 1138 # map for every image with a list of all the symbols and addresses 1139 if frame_img_name and frame_addr and frame_symbol: 1140 description = frame_symbol 1141 frame_offset_value = 0 1142 if frame_offset: 1143 description += " + " + frame_offset 1144 frame_offset_value = int(frame_offset, 0) 1145 if frame_img_name not in self.symbols: 1146 self.symbols[frame_img_name] = list() 1147 self.symbols[frame_img_name].append( 1148 { 1149 "name": frame_symbol, 1150 "address": int(frame_addr, 0) - frame_offset_value, 1151 } 1152 ) 1153 1154 self.thread.frames.append( 1155 self.crashlog.Frame(int(frame_id), int(frame_addr, 0), description) 1156 ) 1157 1158 def parse_images(self, line): 1159 image_match = self.image_regex_uuid.search(line) 1160 if image_match: 1161 ( 1162 img_lo, 1163 img_hi, 1164 img_name, 1165 img_version, 1166 img_uuid, 1167 img_path, 1168 ) = image_match.groups() 1169 1170 image = self.crashlog.DarwinImage( 1171 int(img_lo, 0), 1172 int(img_hi, 0), 1173 img_name.strip(), 1174 img_version.strip() if img_version else "", 1175 uuid.UUID(img_uuid), 1176 img_path, 1177 self.verbose, 1178 ) 1179 unqualified_img_name = os.path.basename(img_path) 1180 if unqualified_img_name in self.symbols: 1181 for symbol in self.symbols[unqualified_img_name]: 1182 image.symbols[symbol["name"]] = { 1183 "name": symbol["name"], 1184 "type": "code", 1185 # NOTE: "address" is actually the symbol image offset 1186 "address": symbol["address"] - int(img_lo, 0), 1187 } 1188 1189 self.images.append(image) 1190 self.crashlog.images.append(image) 1191 else: 1192 print("error: image regex failed for: %s" % line) 1193 1194 def parse_thread_registers(self, line): 1195 # "r12: 0x00007fff6b5939c8 r13: 0x0000000007000006 r14: 0x0000000000002a03 r15: 0x0000000000000c00" 1196 reg_values = re.findall("([a-z0-9]+): (0x[0-9a-f]+)", line, re.I) 1197 for reg, value in reg_values: 1198 self.thread.registers[reg] = int(value, 16) 1199 1200 def parse_system(self, line): 1201 self.crashlog.system_profile.append(line) 1202 1203 def parse_instructions(self, line): 1204 pass 1205 1206 1207def usage(): 1208 print("Usage: lldb-symbolicate.py [-n name] executable-image") 1209 sys.exit(0) 1210 1211 1212def save_crashlog(debugger, command, exe_ctx, result, dict): 1213 usage = "usage: %prog [options] <output-path>" 1214 description = """Export the state of current target into a crashlog file""" 1215 parser = optparse.OptionParser( 1216 description=description, prog="save_crashlog", usage=usage 1217 ) 1218 parser.add_option( 1219 "-v", 1220 "--verbose", 1221 action="store_true", 1222 dest="verbose", 1223 help="display verbose debug info", 1224 default=False, 1225 ) 1226 try: 1227 (options, args) = parser.parse_args(shlex.split(command)) 1228 except: 1229 result.PutCString("error: invalid options") 1230 return 1231 if len(args) != 1: 1232 result.PutCString( 1233 "error: invalid arguments, a single output file is the only valid argument" 1234 ) 1235 return 1236 out_file = open(args[0], "w", encoding="utf-8") 1237 if not out_file: 1238 result.PutCString("error: failed to open file '%s' for writing...", args[0]) 1239 return 1240 target = exe_ctx.target 1241 if target: 1242 identifier = target.executable.basename 1243 process = exe_ctx.process 1244 if process: 1245 pid = process.id 1246 if pid != lldb.LLDB_INVALID_PROCESS_ID: 1247 out_file.write("Process: %s [%u]\n" % (identifier, pid)) 1248 out_file.write("Path: %s\n" % (target.executable.fullpath)) 1249 out_file.write("Identifier: %s\n" % (identifier)) 1250 out_file.write( 1251 "\nDate/Time: %s\n" 1252 % (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) 1253 ) 1254 out_file.write( 1255 "OS Version: Mac OS X %s (%s)\n" 1256 % ( 1257 platform.mac_ver()[0], 1258 subprocess.check_output("sysctl -n kern.osversion", shell=True).decode( 1259 "utf-8" 1260 ), 1261 ) 1262 ) 1263 out_file.write("Report Version: 9\n") 1264 for thread_idx in range(process.num_threads): 1265 thread = process.thread[thread_idx] 1266 out_file.write("\nThread %u:\n" % (thread_idx)) 1267 for frame_idx, frame in enumerate(thread.frames): 1268 frame_pc = frame.pc 1269 frame_offset = 0 1270 if frame.function: 1271 block = frame.GetFrameBlock() 1272 block_range = block.range[frame.addr] 1273 if block_range: 1274 block_start_addr = block_range[0] 1275 frame_offset = frame_pc - block_start_addr.GetLoadAddress( 1276 target 1277 ) 1278 else: 1279 frame_offset = frame_pc - frame.function.addr.GetLoadAddress( 1280 target 1281 ) 1282 elif frame.symbol: 1283 frame_offset = frame_pc - frame.symbol.addr.GetLoadAddress(target) 1284 out_file.write( 1285 "%-3u %-32s 0x%16.16x %s" 1286 % (frame_idx, frame.module.file.basename, frame_pc, frame.name) 1287 ) 1288 if frame_offset > 0: 1289 out_file.write(" + %u" % (frame_offset)) 1290 line_entry = frame.line_entry 1291 if line_entry: 1292 if options.verbose: 1293 # This will output the fullpath + line + column 1294 out_file.write(" %s" % (line_entry)) 1295 else: 1296 out_file.write( 1297 " %s:%u" % (line_entry.file.basename, line_entry.line) 1298 ) 1299 column = line_entry.column 1300 if column: 1301 out_file.write(":%u" % (column)) 1302 out_file.write("\n") 1303 1304 out_file.write("\nBinary Images:\n") 1305 for module in target.modules: 1306 text_segment = module.section["__TEXT"] 1307 if text_segment: 1308 text_segment_load_addr = text_segment.GetLoadAddress(target) 1309 if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS: 1310 text_segment_end_load_addr = ( 1311 text_segment_load_addr + text_segment.size 1312 ) 1313 identifier = module.file.basename 1314 module_version = "???" 1315 module_version_array = module.GetVersion() 1316 if module_version_array: 1317 module_version = ".".join(map(str, module_version_array)) 1318 out_file.write( 1319 " 0x%16.16x - 0x%16.16x %s (%s - ???) <%s> %s\n" 1320 % ( 1321 text_segment_load_addr, 1322 text_segment_end_load_addr, 1323 identifier, 1324 module_version, 1325 module.GetUUIDString(), 1326 module.file.fullpath, 1327 ) 1328 ) 1329 out_file.close() 1330 else: 1331 result.PutCString("error: invalid target") 1332 1333 1334class Symbolicate: 1335 def __init__(self, debugger, internal_dict): 1336 pass 1337 1338 def __call__(self, debugger, command, exe_ctx, result): 1339 SymbolicateCrashLogs(debugger, shlex.split(command), result, True) 1340 1341 def get_short_help(self): 1342 return "Symbolicate one or more darwin crash log files." 1343 1344 def get_long_help(self): 1345 option_parser = CrashLogOptionParser() 1346 return option_parser.format_help() 1347 1348 1349def SymbolicateCrashLog(crash_log, options): 1350 if options.debug: 1351 crash_log.dump() 1352 if not crash_log.images: 1353 print("error: no images in crash log") 1354 return 1355 1356 if options.dump_image_list: 1357 print("Binary Images:") 1358 for image in crash_log.images: 1359 if options.verbose: 1360 print(image.debug_dump()) 1361 else: 1362 print(image) 1363 1364 target = crash_log.create_target() 1365 if not target: 1366 return 1367 1368 if options.load_all_images: 1369 for image in crash_log.images: 1370 image.resolve = True 1371 elif options.crashed_only: 1372 for thread in crash_log.threads: 1373 if thread.did_crash(): 1374 for ident in thread.idents: 1375 for image in crash_log.find_images_with_identifier(ident): 1376 image.resolve = True 1377 1378 futures = [] 1379 loaded_images = [] 1380 with tempfile.TemporaryDirectory() as obj_dir: 1381 with concurrent.futures.ThreadPoolExecutor() as executor: 1382 1383 def add_module(image, target, obj_dir): 1384 return image, image.add_module(target, obj_dir) 1385 1386 for image in crash_log.images: 1387 futures.append( 1388 executor.submit( 1389 add_module, image=image, target=target, obj_dir=obj_dir 1390 ) 1391 ) 1392 for future in concurrent.futures.as_completed(futures): 1393 image, err = future.result() 1394 if err: 1395 print(err) 1396 else: 1397 loaded_images.append(image) 1398 1399 if crash_log.backtraces: 1400 for thread in crash_log.backtraces: 1401 thread.dump_symbolicated(crash_log, options) 1402 print() 1403 1404 for thread in crash_log.threads: 1405 thread.dump_symbolicated(crash_log, options) 1406 print() 1407 1408 if crash_log.errors: 1409 print("Errors:") 1410 for error in crash_log.errors: 1411 print(error) 1412 1413 1414def load_crashlog_in_scripted_process(debugger, crashlog_path, options, result): 1415 crashlog = CrashLogParser.create(debugger, crashlog_path, False).parse() 1416 1417 target = lldb.SBTarget() 1418 # 1. Try to use the user-provided target 1419 if options.target_path: 1420 target = debugger.CreateTarget(options.target_path) 1421 if not target: 1422 raise InteractiveCrashLogException( 1423 "couldn't create target provided by the user (%s)" % options.target_path 1424 ) 1425 1426 # 2. If the user didn't provide a target, try to create a target using the symbolicator 1427 if not target or not target.IsValid(): 1428 target = crashlog.create_target() 1429 # 3. If that didn't work, create a dummy target 1430 if target is None or not target.IsValid(): 1431 arch = crashlog.process_arch 1432 if not arch: 1433 raise InteractiveCrashLogException( 1434 "couldn't create find the architecture to create the target" 1435 ) 1436 target = debugger.CreateTargetWithFileAndArch(None, arch) 1437 # 4. Fail 1438 if target is None or not target.IsValid(): 1439 raise InteractiveCrashLogException("couldn't create target") 1440 1441 ci = debugger.GetCommandInterpreter() 1442 if not ci: 1443 raise InteractiveCrashLogException("couldn't get command interpreter") 1444 1445 ci.HandleCommand("script from lldb.macosx import crashlog_scripted_process", result) 1446 if not result.Succeeded(): 1447 raise InteractiveCrashLogException( 1448 "couldn't import crashlog scripted process module" 1449 ) 1450 1451 structured_data = lldb.SBStructuredData() 1452 structured_data.SetFromJSON( 1453 json.dumps( 1454 {"file_path": crashlog_path, "load_all_images": options.load_all_images} 1455 ) 1456 ) 1457 launch_info = lldb.SBLaunchInfo(None) 1458 launch_info.SetProcessPluginName("ScriptedProcess") 1459 launch_info.SetScriptedProcessClassName( 1460 "crashlog_scripted_process.CrashLogScriptedProcess" 1461 ) 1462 launch_info.SetScriptedProcessDictionary(structured_data) 1463 launch_info.SetLaunchFlags(lldb.eLaunchFlagStopAtEntry) 1464 1465 error = lldb.SBError() 1466 process = target.Launch(launch_info, error) 1467 1468 if not process or error.Fail(): 1469 raise InteractiveCrashLogException("couldn't launch Scripted Process", error) 1470 1471 process.GetScriptedImplementation().set_crashlog(crashlog) 1472 process.Continue() 1473 1474 if not options.skip_status: 1475 1476 @contextlib.contextmanager 1477 def synchronous(debugger): 1478 async_state = debugger.GetAsync() 1479 debugger.SetAsync(False) 1480 try: 1481 yield 1482 finally: 1483 debugger.SetAsync(async_state) 1484 1485 with synchronous(debugger): 1486 run_options = lldb.SBCommandInterpreterRunOptions() 1487 run_options.SetStopOnError(True) 1488 run_options.SetStopOnCrash(True) 1489 run_options.SetEchoCommands(True) 1490 1491 commands_stream = lldb.SBStream() 1492 commands_stream.Print("process status --verbose\n") 1493 commands_stream.Print("thread backtrace --extended true\n") 1494 error = debugger.SetInputString(commands_stream.GetData()) 1495 if error.Success(): 1496 debugger.RunCommandInterpreter(True, False, run_options, 0, False, True) 1497 1498 1499def CreateSymbolicateCrashLogOptions( 1500 command_name, description, add_interactive_options 1501): 1502 usage = "usage: %prog [options] <FILE> [FILE ...]" 1503 option_parser = optparse.OptionParser( 1504 description=description, prog="crashlog", usage=usage 1505 ) 1506 option_parser.add_option( 1507 "--version", 1508 "-V", 1509 dest="version", 1510 action="store_true", 1511 help="Show crashlog version", 1512 default=False, 1513 ) 1514 option_parser.add_option( 1515 "--verbose", 1516 "-v", 1517 action="store_true", 1518 dest="verbose", 1519 help="display verbose debug info", 1520 default=False, 1521 ) 1522 option_parser.add_option( 1523 "--debug", 1524 "-g", 1525 action="store_true", 1526 dest="debug", 1527 help="display verbose debug logging", 1528 default=False, 1529 ) 1530 option_parser.add_option( 1531 "--load-all", 1532 "-a", 1533 action="store_true", 1534 dest="load_all_images", 1535 help="load all executable images, not just the images found in the " 1536 "crashed stack frames, loads stackframes for all the threads in " 1537 "interactive mode.", 1538 default=False, 1539 ) 1540 option_parser.add_option( 1541 "--images", 1542 action="store_true", 1543 dest="dump_image_list", 1544 help="show image list", 1545 default=False, 1546 ) 1547 option_parser.add_option( 1548 "--debug-delay", 1549 type="int", 1550 dest="debug_delay", 1551 metavar="NSEC", 1552 help="pause for NSEC seconds for debugger", 1553 default=0, 1554 ) 1555 option_parser.add_option( 1556 "--crashed-only", 1557 "-c", 1558 action="store_true", 1559 dest="crashed_only", 1560 help="only symbolicate the crashed thread", 1561 default=False, 1562 ) 1563 option_parser.add_option( 1564 "--disasm-depth", 1565 "-d", 1566 type="int", 1567 dest="disassemble_depth", 1568 help="set the depth in stack frames that should be disassembled (default is 1)", 1569 default=1, 1570 ) 1571 option_parser.add_option( 1572 "--disasm-all", 1573 "-D", 1574 action="store_true", 1575 dest="disassemble_all_threads", 1576 help="enabled disassembly of frames on all threads (not just the crashed thread)", 1577 default=False, 1578 ) 1579 option_parser.add_option( 1580 "--disasm-before", 1581 "-B", 1582 type="int", 1583 dest="disassemble_before", 1584 help="the number of instructions to disassemble before the frame PC", 1585 default=4, 1586 ) 1587 option_parser.add_option( 1588 "--disasm-after", 1589 "-A", 1590 type="int", 1591 dest="disassemble_after", 1592 help="the number of instructions to disassemble after the frame PC", 1593 default=4, 1594 ) 1595 option_parser.add_option( 1596 "--source-context", 1597 "-C", 1598 type="int", 1599 metavar="NLINES", 1600 dest="source_context", 1601 help="show NLINES source lines of source context (default = 4)", 1602 default=4, 1603 ) 1604 option_parser.add_option( 1605 "--source-frames", 1606 type="int", 1607 metavar="NFRAMES", 1608 dest="source_frames", 1609 help="show source for NFRAMES (default = 4)", 1610 default=4, 1611 ) 1612 option_parser.add_option( 1613 "--source-all", 1614 action="store_true", 1615 dest="source_all", 1616 help="show source for all threads, not just the crashed thread", 1617 default=False, 1618 ) 1619 if add_interactive_options: 1620 option_parser.add_option( 1621 "-i", 1622 "--interactive", 1623 action="store_true", 1624 help="parse a crash log and load it in a ScriptedProcess", 1625 default=False, 1626 ) 1627 option_parser.add_option( 1628 "-b", 1629 "--batch", 1630 action="store_true", 1631 help="dump symbolicated stackframes without creating a debug session", 1632 default=True, 1633 ) 1634 option_parser.add_option( 1635 "--target", 1636 "-t", 1637 dest="target_path", 1638 help="the target binary path that should be used for interactive crashlog (optional)", 1639 default=None, 1640 ) 1641 option_parser.add_option( 1642 "--skip-status", 1643 "-s", 1644 dest="skip_status", 1645 action="store_true", 1646 help="prevent the interactive crashlog to dump the process status and thread backtrace at launch", 1647 default=False, 1648 ) 1649 return option_parser 1650 1651 1652def CrashLogOptionParser(): 1653 description = """Symbolicate one or more darwin crash log files to provide source file and line information, 1654inlined stack frames back to the concrete functions, and disassemble the location of the crash 1655for the first frame of the crashed thread. 1656If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter 1657for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been 1658created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows 1659you to explore the program as if it were stopped at the locations described in the crash log and functions can 1660be disassembled and lookups can be performed using the addresses found in the crash log.""" 1661 return CreateSymbolicateCrashLogOptions("crashlog", description, True) 1662 1663 1664def SymbolicateCrashLogs(debugger, command_args, result, is_command): 1665 option_parser = CrashLogOptionParser() 1666 1667 if not len(command_args): 1668 option_parser.print_help() 1669 return 1670 1671 try: 1672 (options, args) = option_parser.parse_args(command_args) 1673 except: 1674 return 1675 1676 # Interactive mode requires running the crashlog command from inside lldb. 1677 if options.interactive and not is_command: 1678 lldb_exec = ( 1679 subprocess.check_output(["/usr/bin/xcrun", "-f", "lldb"]) 1680 .decode("utf-8") 1681 .strip() 1682 ) 1683 sys.exit( 1684 os.execv( 1685 lldb_exec, 1686 [ 1687 lldb_exec, 1688 "-o", 1689 "command script import lldb.macosx", 1690 "-o", 1691 "crashlog {}".format(shlex.join(command_args)), 1692 ], 1693 ) 1694 ) 1695 1696 if options.version: 1697 print(debugger.GetVersionString()) 1698 return 1699 1700 if options.debug: 1701 print("command_args = %s" % command_args) 1702 print("options", options) 1703 print("args", args) 1704 1705 if options.debug_delay > 0: 1706 print("Waiting %u seconds for debugger to attach..." % options.debug_delay) 1707 time.sleep(options.debug_delay) 1708 error = lldb.SBError() 1709 1710 def should_run_in_interactive_mode(options, ci): 1711 if options.interactive: 1712 return True 1713 elif options.batch: 1714 return False 1715 # elif ci and ci.IsInteractive(): 1716 # return True 1717 else: 1718 return False 1719 1720 ci = debugger.GetCommandInterpreter() 1721 1722 if args: 1723 for crashlog_file in args: 1724 crashlog_path = os.path.expanduser(crashlog_file) 1725 if not os.path.exists(crashlog_path): 1726 raise FileNotFoundError( 1727 "crashlog file %s does not exist" % crashlog_path 1728 ) 1729 if should_run_in_interactive_mode(options, ci): 1730 try: 1731 load_crashlog_in_scripted_process( 1732 debugger, crashlog_path, options, result 1733 ) 1734 except InteractiveCrashLogException as e: 1735 result.SetError(str(e)) 1736 else: 1737 crash_log = CrashLogParser.create( 1738 debugger, crashlog_path, options.verbose 1739 ).parse() 1740 SymbolicateCrashLog(crash_log, options) 1741 1742 1743if __name__ == "__main__": 1744 # Create a new debugger instance 1745 debugger = lldb.SBDebugger.Create() 1746 result = lldb.SBCommandReturnObject() 1747 SymbolicateCrashLogs(debugger, sys.argv[1:], result, False) 1748 lldb.SBDebugger.Destroy(debugger) 1749 1750 1751def __lldb_init_module(debugger, internal_dict): 1752 debugger.HandleCommand( 1753 "command script add -o -c lldb.macosx.crashlog.Symbolicate -C disk-file crashlog" 1754 ) 1755 debugger.HandleCommand( 1756 "command script add -o -f lldb.macosx.crashlog.save_crashlog -C disk-file save_crashlog" 1757 ) 1758 print( 1759 '"crashlog" and "save_crashlog" commands have been installed, use ' 1760 'the "--help" options on these commands for detailed help.' 1761 ) 1762