1919 lines · python
1#!/usr/bin/env python32 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.py9# lldb10# (lldb) script import crashlog11# "crashlog" command installed, type "crashlog --help" for detailed help12# (lldb) crashlog ~/Library/Logs/DiagnosticReports/a.crash13#14# The benefit of running the crashlog command inside lldb in the15# embedded python interpreter is when the command completes, there16# will be a target with all of the files loaded at the locations17# described in the crash log. Only the files that have stack frames18# in the backtrace will be loaded unless the "--load-all" option19# has been specified. This allows users to explore the program in the20# 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.crash27# ----------------------------------------------------------------------28 29import abc30import argparse31import concurrent.futures32import contextlib33import datetime34import enum35import json36import os37import platform38import plistlib39import re40import shlex41import string42import subprocess43import sys44import tempfile45import threading46import time47import uuid48 49print_lock = threading.RLock()50 51try:52 # First try for LLDB in case PYTHONPATH is already correctly setup.53 import lldb54except ImportError:55 # Ask the command line driver for the path to the lldb module. Copy over56 # 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 lldb67 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 symbolication74from lldb.plugins.scripted_process import INTEL64_GPR, ARM64_GPR75 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 = index90 self.id = index91 self.images = list()92 self.frames = list()93 self.idents = list()94 self.registers = dict()95 self.reason = None96 self.name = None97 self.queue = None98 self.crashed = False99 self.app_specific_backtrace = app_specific_backtrace100 self.arch = arch101 102 def dump_registers(self, prefix=""):103 registers_info = None104 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_GPR114 elif "arm64" in self.arch:115 registers_info = ARM64_GPR116 else:117 print("unknown target architecture: %s" % self.arch)118 return119 120 # Add registers available in the register information dictionary.121 for reg_info in registers_info:122 reg_name = None123 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.registers127 ):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 continue132 133 reg_val = self.registers[reg_name]134 sorted_registers[reg_name] = reg_val135 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_backtrace167 if not this_thread_crashed:168 this_thread_crashed = self.did_crash()169 if options.crashed_only and not this_thread_crashed:170 return171 172 print("%s" % self)173 display_frame_idx = -1174 for frame_idx, frame in enumerate(self.frames):175 disassemble = (176 this_thread_crashed or options.disassemble_all_threads177 ) and frame_idx < options.disassemble_depth178 179 # Except for the zeroth frame, we should subtract 1 from every180 # frame pc to get the previous line entry.181 pc = frame.pc & crash_log.addr_mask182 pc = pc if frame_idx == 0 or pc == 0 else pc - 1183 symbolicated_frame_addresses = crash_log.symbolicate(184 pc, options.verbose185 )186 187 if symbolicated_frame_addresses:188 symbolicated_frame_address_idx = 0189 for symbolicated_frame_address in symbolicated_frame_addresses:190 display_frame_idx += 1191 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_frames195 and options.source_context196 ):197 source_context = options.source_context198 line_entry = (199 symbolicated_frame_address.get_symbol_context().line_entry200 )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 bit215 indent_str = " "216 join_str = "\n" + indent_str217 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 += 1241 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.crashed256 257 def __str__(self):258 if self.app_specific_backtrace:259 s = "Application Specific Backtrace[%u]" % self.index260 else:261 s = "Thread[%u]" % self.index262 if self.reason:263 s += " %s" % self.reason264 return s265 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 = pc271 self.description = description272 self.index = index273 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 "LLDB_APPLE_DSYMFORUUID_EXECUTABLE" in os.environ:288 dsymForUUIDBinary = os.environ["LLDB_APPLE_DSYMFORUUID_EXECUTABLE"]289 elif not os.path.exists(dsymForUUIDBinary):290 try:291 dsymForUUIDBinary = (292 subprocess.check_output("which dsymForUUID", shell=True)293 .decode("utf-8")294 .rstrip("\n")295 )296 except:297 dsymForUUIDBinary = ""298 299 dwarfdump_uuid_regex = re.compile(r"UUID: ([-0-9a-fA-F]+) \(([^\(]+)\) .*")300 301 def __init__(302 self, text_addr_lo, text_addr_hi, identifier, version, uuid, path, verbose303 ):304 symbolication.Image.__init__(self, path, uuid)305 self.add_section(306 symbolication.Section(text_addr_lo, text_addr_hi, "__TEXT")307 )308 self.identifier = identifier309 self.version = version310 self.verbose = verbose311 312 def show_symbol_progress(self):313 """314 Hide progress output and errors from system frameworks as they are plentiful.315 """316 if self.verbose:317 return True318 return not (319 self.path.startswith("/System/Library/")320 or self.path.startswith("/usr/lib/")321 )322 323 def find_matching_slice(self):324 dwarfdump_cmd_output = subprocess.check_output(325 'dwarfdump --uuid "%s"' % self.path, shell=True326 ).decode("utf-8")327 self_uuid = self.get_uuid()328 for line in dwarfdump_cmd_output.splitlines():329 match = self.dwarfdump_uuid_regex.search(line)330 if match:331 dwarf_uuid_str = match.group(1)332 dwarf_uuid = uuid.UUID(dwarf_uuid_str)333 if self_uuid == dwarf_uuid:334 self.resolved_path = self.path335 self.arch = match.group(2)336 return True337 if not self.resolved_path:338 self.unavailable = True339 if self.show_symbol_progress():340 print(341 (342 "error\n error: unable to locate '%s' with UUID %s"343 % (self.path, self.get_normalized_uuid_string())344 )345 )346 return False347 348 def locate_module_and_debug_symbols(self):349 # Don't load a module twice...350 if self.resolved:351 return True352 # Mark this as resolved so we don't keep trying353 self.resolved = True354 uuid_str = self.get_normalized_uuid_string()355 if self.show_symbol_progress():356 with print_lock:357 print("Getting symbols for %s %s..." % (uuid_str, self.path))358 # Keep track of unresolved source paths.359 unavailable_source_paths = set()360 if os.path.exists(self.dsymForUUIDBinary):361 dsym_for_uuid_command = (362 "{} --copyExecutable --ignoreNegativeCache {}".format(363 self.dsymForUUIDBinary, uuid_str364 )365 )366 s = subprocess.check_output(dsym_for_uuid_command, shell=True)367 if s:368 try:369 plist_root = read_plist(s)370 except:371 with print_lock:372 print(373 (374 "Got exception: ",375 sys.exc_info()[1],376 " handling dsymForUUID output: \n",377 s,378 )379 )380 raise381 if plist_root:382 plist = plist_root[uuid_str]383 if plist:384 if "DBGArchitecture" in plist:385 self.arch = plist["DBGArchitecture"]386 if "DBGDSYMPath" in plist:387 self.symfile = os.path.realpath(plist["DBGDSYMPath"])388 if "DBGSymbolRichExecutable" in plist:389 self.path = os.path.expanduser(390 plist["DBGSymbolRichExecutable"]391 )392 self.resolved_path = self.path393 if "DBGSourcePathRemapping" in plist:394 path_remapping = plist["DBGSourcePathRemapping"]395 for _, value in path_remapping.items():396 source_path = os.path.expanduser(value)397 if not os.path.exists(source_path):398 unavailable_source_paths.add(source_path)399 if not self.resolved_path and os.path.exists(self.path):400 if not self.find_matching_slice():401 return False402 if not self.resolved_path and not os.path.exists(self.path):403 try:404 mdfind_results = (405 subprocess.check_output(406 [407 "/usr/bin/mdfind",408 "com_apple_xcode_dsym_uuids == %s" % uuid_str,409 ]410 )411 .decode("utf-8")412 .splitlines()413 )414 found_matching_slice = False415 for dsym in mdfind_results:416 dwarf_dir = os.path.join(dsym, "Contents/Resources/DWARF")417 if not os.path.exists(dwarf_dir):418 # Not a dSYM bundle, probably an Xcode archive.419 continue420 with print_lock:421 print('falling back to binary inside "%s"' % dsym)422 self.symfile = dsym423 # Look for the executable next to the dSYM bundle.424 parent_dir = os.path.dirname(dsym)425 executables = []426 for root, _, files in os.walk(parent_dir):427 for file in files:428 abs_path = os.path.join(root, file)429 if os.path.isfile(abs_path) and os.access(430 abs_path, os.X_OK431 ):432 executables.append(abs_path)433 for binary in executables:434 basename = os.path.basename(binary)435 if basename == self.identifier:436 self.path = binary437 found_matching_slice = True438 break439 if found_matching_slice:440 break441 except:442 pass443 if (self.resolved_path and os.path.exists(self.resolved_path)) or (444 self.path and os.path.exists(self.path)445 ):446 with print_lock:447 print("Resolved symbols for %s %s..." % (uuid_str, self.path))448 if len(unavailable_source_paths):449 for source_path in unavailable_source_paths:450 print(451 "Could not access remapped source path for %s %s"452 % (uuid_str, source_path)453 )454 return True455 else:456 self.unavailable = True457 return False458 459 def __init__(self, debugger, path, verbose):460 """CrashLog constructor that take a path to a darwin crash log file"""461 symbolication.Symbolicator.__init__(self, debugger)462 self.path = os.path.expanduser(path)463 self.info_lines = list()464 self.system_profile = list()465 self.threads = list()466 self.backtraces = list() # For application specific backtraces467 self.idents = (468 list()469 ) # A list of the required identifiers for doing all stack backtraces470 self.errors = list()471 self.exception = dict()472 self.crashed_thread_idx = -1473 self.version = -1474 self.target = None475 self.verbose = verbose476 self.process_id = None477 self.process_identifier = None478 self.process_path = None479 self.process_arch = None480 481 def dump(self):482 print("Crash Log File: %s" % (self.path))483 if self.backtraces:484 print("\nApplication Specific Backtraces:")485 for thread in self.backtraces:486 thread.dump(" ")487 print("\nThreads:")488 for thread in self.threads:489 thread.dump(" ")490 print("\nImages:")491 for image in self.images:492 image.dump(" ")493 494 def set_main_image(self, identifier):495 for i, image in enumerate(self.images):496 if image.identifier == identifier:497 self.images.insert(0, self.images.pop(i))498 break499 500 def find_image_with_identifier(self, identifier):501 for image in self.images:502 if image.identifier == identifier:503 return image504 regex_text = r"^.*\.%s$" % (re.escape(identifier))505 regex = re.compile(regex_text)506 for image in self.images:507 if regex.match(image.identifier):508 return image509 return None510 511 def create_target(self):512 if self.target is None:513 self.target = symbolication.Symbolicator.create_target(self)514 if self.target:515 return self.target516 # We weren't able to open the main executable as, but we can still517 # symbolicate518 print("crashlog.create_target()...2")519 if self.idents:520 for ident in self.idents:521 image = self.find_image_with_identifier(ident)522 if image:523 self.target = image.create_target(self.debugger)524 if self.target:525 return self.target # success526 print("crashlog.create_target()...3")527 for image in self.images:528 self.target = image.create_target(self.debugger)529 if self.target:530 return self.target # success531 print("crashlog.create_target()...4")532 print("error: Unable to locate any executables from the crash log.")533 print(" Try loading the executable into lldb before running crashlog")534 print(535 " and/or make sure the .dSYM bundles can be found by Spotlight."536 )537 return self.target538 539 def get_target(self):540 return self.target541 542 def load_images(self, options, loaded_images=None):543 if not loaded_images:544 loaded_images = []545 images_to_load = self.images546 if options.load_all_images:547 for image in self.images:548 image.resolve = True549 elif options.crashed_only:550 images_to_load = []551 for thread in self.threads:552 if thread.did_crash() or thread.app_specific_backtrace:553 for ident in thread.idents:554 for image in self.find_images_with_identifier(ident):555 image.resolve = True556 images_to_load.append(image)557 558 futures = []559 with tempfile.TemporaryDirectory() as obj_dir:560 561 def add_module(image, target, obj_dir):562 return image, image.add_module(target, obj_dir)563 564 max_worker = None565 if options.no_parallel_image_loading:566 max_worker = 1567 568 with concurrent.futures.ThreadPoolExecutor(max_worker) as executor:569 for image in images_to_load:570 if image not in loaded_images:571 if image.uuid == uuid.UUID(int=0):572 continue573 futures.append(574 executor.submit(575 add_module,576 image=image,577 target=self.target,578 obj_dir=obj_dir,579 )580 )581 582 for future in concurrent.futures.as_completed(futures):583 image, err = future.result()584 if err:585 print(err)586 else:587 loaded_images.append(image)588 589 590class CrashLogFormatException(Exception):591 pass592 593 594class CrashLogParseException(Exception):595 pass596 597 598class InteractiveCrashLogException(Exception):599 pass600 601 602class CrashLogParser:603 @staticmethod604 def create(debugger, path, options):605 data = JSONCrashLogParser.is_valid_json(path)606 if data:607 parser = JSONCrashLogParser(debugger, path, options)608 parser.data = data609 return parser610 else:611 return TextCrashLogParser(debugger, path, options)612 613 def __init__(self, debugger, path, options):614 self.path = os.path.expanduser(path)615 self.options = options616 self.crashlog = CrashLog(debugger, self.path, self.options.verbose)617 618 @abc.abstractmethod619 def parse(self):620 pass621 622 623class JSONCrashLogParser(CrashLogParser):624 @staticmethod625 def is_valid_json(path):626 def parse_json(buffer):627 try:628 return json.loads(buffer)629 except:630 # The first line can contain meta data. Try stripping it and631 # try again.632 head, _, tail = buffer.partition("\n")633 return json.loads(tail)634 635 with open(path, "r", encoding="utf-8") as f:636 buffer = f.read()637 try:638 return parse_json(buffer)639 except:640 return None641 642 def __init__(self, debugger, path, options):643 super().__init__(debugger, path, options)644 645 def parse(self):646 try:647 self.parse_process_info(self.data)648 self.parse_images(self.data["usedImages"])649 self.parse_main_image(self.data)650 self.parse_threads(self.data["threads"])651 if "asi" in self.data:652 self.crashlog.asi = self.data["asi"]653 # FIXME: With the current design, we can either show the ASI or Last654 # Exception Backtrace, not both. Is there a situation where we would655 # like to show both ?656 if "asiBacktraces" in self.data:657 self.parse_app_specific_backtraces(self.data["asiBacktraces"])658 if "lastExceptionBacktrace" in self.data:659 self.parse_last_exception_backtraces(660 self.data["lastExceptionBacktrace"]661 )662 self.parse_errors(self.data)663 thread = self.crashlog.threads[self.crashlog.crashed_thread_idx]664 reason = self.parse_crash_reason(self.data["exception"])665 if thread.reason:666 thread.reason = "{} {}".format(thread.reason, reason)667 else:668 thread.reason = reason669 except (KeyError, ValueError, TypeError) as e:670 raise CrashLogParseException(671 "Failed to parse JSON crashlog: {}: {}".format(type(e).__name__, e)672 )673 674 return self.crashlog675 676 def get_used_image(self, idx):677 return self.data["usedImages"][idx]678 679 def parse_process_info(self, json_data):680 self.crashlog.process_id = json_data["pid"]681 self.crashlog.process_identifier = json_data["procName"]682 if "procPath" in json_data:683 self.crashlog.process_path = json_data["procPath"]684 685 def parse_crash_reason(self, json_exception):686 self.crashlog.exception = json_exception687 exception_type = json_exception["type"]688 exception_signal = " "689 if "signal" in json_exception:690 exception_signal += "({})".format(json_exception["signal"])691 692 if "codes" in json_exception:693 exception_extra = " ({})".format(json_exception["codes"])694 elif "subtype" in json_exception:695 exception_extra = " ({})".format(json_exception["subtype"])696 else:697 exception_extra = ""698 return "{}{}{}".format(exception_type, exception_signal, exception_extra)699 700 def parse_images(self, json_images):701 for json_image in json_images:702 img_uuid = uuid.UUID(json_image["uuid"])703 low = int(json_image["base"])704 high = low + int(json_image["size"]) if "size" in json_image else low705 name = json_image["name"] if "name" in json_image else ""706 path = json_image["path"] if "path" in json_image else ""707 version = ""708 darwin_image = self.crashlog.DarwinImage(709 low, high, name, version, img_uuid, path, self.options.verbose710 )711 if "arch" in json_image:712 darwin_image.arch = json_image["arch"]713 if path == self.crashlog.process_path:714 self.crashlog.process_arch = darwin_image.arch715 self.crashlog.images.append(darwin_image)716 717 def parse_main_image(self, json_data):718 if "procName" in json_data:719 proc_name = json_data["procName"]720 self.crashlog.set_main_image(proc_name)721 722 def parse_frames(self, thread, json_frames):723 idx = 0724 for json_frame in json_frames:725 image_id = int(json_frame["imageIndex"])726 json_image = self.get_used_image(image_id)727 ident = json_image["name"] if "name" in json_image else ""728 thread.add_ident(ident)729 if ident not in self.crashlog.idents:730 self.crashlog.idents.append(ident)731 732 frame_offset = int(json_frame["imageOffset"])733 image_addr = self.get_used_image(image_id)["base"]734 pc = image_addr + frame_offset735 736 if "symbol" in json_frame:737 symbol = json_frame["symbol"]738 location = 0739 if "symbolLocation" in json_frame and json_frame["symbolLocation"]:740 location = int(json_frame["symbolLocation"])741 image = self.crashlog.images[image_id]742 image.symbols[symbol] = {743 "name": symbol,744 "type": "code",745 "address": frame_offset - location,746 }747 748 thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset))749 750 # on arm64 systems, if it jump through a null function pointer,751 # we end up at address 0 and the crash reporter unwinder752 # misses the frame that actually faulted.753 # But $lr can tell us where the last BL/BLR instruction used754 # was at, so insert that address as the caller stack frame.755 if idx == 0 and pc == 0 and "lr" in thread.registers:756 pc = thread.registers["lr"]757 for image in self.data["usedImages"]:758 text_lo = image["base"]759 text_hi = text_lo + image["size"]760 if text_lo <= pc < text_hi:761 idx += 1762 frame_offset = pc - text_lo763 thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset))764 break765 766 idx += 1767 768 def parse_threads(self, json_threads):769 idx = 0770 for json_thread in json_threads:771 thread = self.crashlog.Thread(idx, False, self.crashlog.process_arch)772 if "name" in json_thread:773 thread.name = json_thread["name"]774 thread.reason = json_thread["name"]775 if "id" in json_thread:776 thread.id = int(json_thread["id"])777 if json_thread.get("triggered", False):778 self.crashlog.crashed_thread_idx = idx779 thread.crashed = True780 if "threadState" in json_thread:781 thread.registers = self.parse_thread_registers(782 json_thread["threadState"]783 )784 if "queue" in json_thread:785 thread.queue = json_thread.get("queue")786 self.parse_frames(thread, json_thread.get("frames", []))787 self.crashlog.threads.append(thread)788 idx += 1789 790 def parse_asi_backtrace(self, thread, bt):791 for line in bt.split("\n"):792 frame_match = TextCrashLogParser.frame_regex.search(line)793 if not frame_match:794 print("error: can't parse application specific backtrace.")795 return False796 797 frame_id = (798 frame_img_name799 ) = (800 frame_addr801 ) = (802 frame_symbol803 ) = frame_offset = frame_file = frame_line = frame_column = None804 805 if len(frame_match.groups()) == 3:806 # Get the image UUID from the frame image name.807 (frame_id, frame_img_name, frame_addr) = frame_match.groups()808 elif len(frame_match.groups()) == 5:809 (810 frame_id,811 frame_img_name,812 frame_addr,813 frame_symbol,814 frame_offset,815 ) = frame_match.groups()816 elif len(frame_match.groups()) == 7:817 (818 frame_id,819 frame_img_name,820 frame_addr,821 frame_symbol,822 frame_offset,823 frame_file,824 frame_line,825 ) = frame_match.groups()826 elif len(frame_match.groups()) == 8:827 (828 frame_id,829 frame_img_name,830 frame_addr,831 frame_symbol,832 frame_offset,833 frame_file,834 frame_line,835 frame_column,836 ) = frame_match.groups()837 838 thread.add_ident(frame_img_name)839 if frame_img_name not in self.crashlog.idents:840 self.crashlog.idents.append(frame_img_name)841 842 description = ""843 if frame_img_name and frame_addr and frame_symbol:844 description = frame_symbol845 frame_offset_value = 0846 if frame_offset:847 description += " + " + frame_offset848 frame_offset_value = int(frame_offset, 0)849 for image in self.crashlog.images:850 if image.identifier == frame_img_name:851 image.symbols[frame_symbol] = {852 "name": frame_symbol,853 "type": "code",854 "address": int(frame_addr, 0) - frame_offset_value,855 }856 857 thread.frames.append(858 self.crashlog.Frame(int(frame_id), int(frame_addr, 0), description)859 )860 861 return True862 863 def parse_app_specific_backtraces(self, json_app_specific_bts):864 thread = self.crashlog.Thread(865 len(self.crashlog.threads), True, self.crashlog.process_arch866 )867 thread.name = "Application Specific Backtrace"868 if self.parse_asi_backtrace(thread, json_app_specific_bts[0]):869 self.crashlog.threads.append(thread)870 else:871 print("error: Couldn't parse Application Specific Backtrace.")872 873 def parse_last_exception_backtraces(self, json_last_exc_bts):874 thread = self.crashlog.Thread(875 len(self.crashlog.threads), True, self.crashlog.process_arch876 )877 thread.name = "Last Exception Backtrace"878 self.parse_frames(thread, json_last_exc_bts)879 self.crashlog.threads.append(thread)880 881 def parse_thread_registers(self, json_thread_state, prefix=None):882 registers = dict()883 for key, state in json_thread_state.items():884 if key == "rosetta":885 registers.update(self.parse_thread_registers(state))886 continue887 if key == "x":888 gpr_dict = {str(idx): reg for idx, reg in enumerate(state)}889 registers.update(self.parse_thread_registers(gpr_dict, key))890 continue891 if key == "flavor":892 if not self.crashlog.process_arch:893 if state == "ARM_THREAD_STATE64":894 self.crashlog.process_arch = "arm64"895 elif state == "X86_THREAD_STATE":896 self.crashlog.process_arch = "x86_64"897 continue898 try:899 value = int(state["value"])900 registers["{}{}".format(prefix or "", key)] = value901 except (KeyError, ValueError, TypeError):902 pass903 return registers904 905 def parse_errors(self, json_data):906 if "reportNotes" in json_data:907 self.crashlog.errors = json_data["reportNotes"]908 909 910class TextCrashLogParser(CrashLogParser):911 parent_process_regex = re.compile(r"^Parent Process:\s*(.*)\[(\d+)\]")912 thread_state_regex = re.compile(r"^Thread (\d+ crashed with|State)")913 thread_instrs_regex = re.compile(r"^Thread \d+ instruction stream")914 thread_regex = re.compile(r"^Thread (\d+).*")915 app_backtrace_regex = re.compile(r"^Application Specific Backtrace (\d+).*")916 917 class VersionRegex:918 version = r"\(.+\)|(?:arm|x86_)[0-9a-z]+"919 920 class FrameRegex(VersionRegex):921 @classmethod922 def get(cls):923 index = r"^(\d+)\s+"924 img_name = r"(.+?)\s+"925 version = r"(?:" + super().version + r"\s+)?"926 address = r"(0x[0-9a-fA-F]{4,})" # 4 digits or more927 928 symbol = r"""929 (?:930 [ ]+931 (?P<symbol>.+)932 (?:933 [ ]\+[ ]934 (?P<symbol_offset>\d+)935 )936 (?:937 [ ]\(938 (?P<file_name>[^:]+):(?P<line_number>\d+)939 (?:940 :(?P<column_num>\d+)941 )?942 )?943 )?944 """945 946 return re.compile(947 index + img_name + version + address + symbol, flags=re.VERBOSE948 )949 950 frame_regex = FrameRegex.get()951 null_frame_regex = re.compile(r"^\d+\s+\?\?\?\s+0{4,} +")952 image_regex_uuid = re.compile(953 r"(0x[0-9a-fA-F]+)" # img_lo954 r"\s+-\s+" # -955 r"(0x[0-9a-fA-F]+)\s+" # img_hi956 r"[+]?(.+?)\s+" # img_name957 r"(?:(" + VersionRegex.version + r")\s+)?" # img_version958 r"(?:<([-0-9a-fA-F]+)>\s+)?" # img_uuid959 r"(\?+|/.*)" # img_path960 )961 exception_type_regex = re.compile(962 r"^Exception Type:\s+(EXC_[A-Z_]+)(?:\s+\((.*)\))?"963 )964 exception_codes_regex = re.compile(965 r"^Exception Codes:\s+(0x[0-9a-fA-F]+),\s*(0x[0-9a-fA-F]+)"966 )967 exception_extra_regex = re.compile(r"^Exception\s+.*:\s+(.*)")968 969 class CrashLogParseMode:970 NORMAL = 0971 THREAD = 1972 IMAGES = 2973 THREGS = 3974 SYSTEM = 4975 INSTRS = 5976 977 def __init__(self, debugger, path, options):978 super().__init__(debugger, path, options)979 self.thread = None980 self.app_specific_backtrace = False981 self.parse_mode = self.CrashLogParseMode.NORMAL982 self.parsers = {983 self.CrashLogParseMode.NORMAL: self.parse_normal,984 self.CrashLogParseMode.THREAD: self.parse_thread,985 self.CrashLogParseMode.IMAGES: self.parse_images,986 self.CrashLogParseMode.THREGS: self.parse_thread_registers,987 self.CrashLogParseMode.SYSTEM: self.parse_system,988 self.CrashLogParseMode.INSTRS: self.parse_instructions,989 }990 self.symbols = {}991 992 def parse(self):993 with open(self.path, "r", encoding="utf-8") as f:994 lines = f.read().splitlines()995 996 idx = 0997 lines_count = len(lines)998 while True:999 if idx >= lines_count:1000 break1001 1002 line = lines[idx]1003 line_len = len(line)1004 1005 if line_len == 0:1006 if self.thread:1007 if self.parse_mode == self.CrashLogParseMode.THREAD:1008 if self.thread.index == self.crashlog.crashed_thread_idx:1009 self.thread.reason = ""1010 if hasattr(self.crashlog, "thread_exception"):1011 self.thread.reason += self.crashlog.thread_exception1012 if hasattr(self.crashlog, "thread_exception_data"):1013 self.thread.reason += (1014 " (%s)" % self.crashlog.thread_exception_data1015 )1016 self.thread.crashed = True1017 if self.app_specific_backtrace:1018 self.crashlog.backtraces.append(self.thread)1019 else:1020 self.crashlog.threads.append(self.thread)1021 self.thread = None1022 1023 empty_lines = 11024 while (1025 idx + empty_lines < lines_count1026 and len(lines[idx + empty_lines]) == 01027 ):1028 empty_lines = empty_lines + 11029 1030 if (1031 empty_lines == 11032 and idx + empty_lines < lines_count - 11033 and self.parse_mode != self.CrashLogParseMode.NORMAL1034 ):1035 # check if next line can be parsed with the current parse mode1036 next_line_idx = idx + empty_lines1037 if self.parsers[self.parse_mode](lines[next_line_idx]):1038 # If that suceeded, skip the empty line and the next line.1039 idx = next_line_idx + 11040 continue1041 self.parse_mode = self.CrashLogParseMode.NORMAL1042 1043 self.parsers[self.parse_mode](line)1044 1045 idx = idx + 11046 1047 return self.crashlog1048 1049 def parse_exception(self, line):1050 if not line.startswith("Exception"):1051 return False1052 if line.startswith("Exception Type:"):1053 self.crashlog.thread_exception = line[15:].strip()1054 exception_type_match = self.exception_type_regex.search(line)1055 if exception_type_match:1056 exc_type, exc_signal = exception_type_match.groups()1057 self.crashlog.exception["type"] = exc_type1058 if exc_signal:1059 self.crashlog.exception["signal"] = exc_signal1060 elif line.startswith("Exception Subtype:"):1061 self.crashlog.thread_exception_subtype = line[18:].strip()1062 if "type" in self.crashlog.exception:1063 self.crashlog.exception[1064 "subtype"1065 ] = self.crashlog.thread_exception_subtype1066 elif line.startswith("Exception Codes:"):1067 self.crashlog.thread_exception_data = line[16:].strip()1068 if "type" not in self.crashlog.exception:1069 return False1070 exception_codes_match = self.exception_codes_regex.search(line)1071 if exception_codes_match:1072 self.crashlog.exception["codes"] = self.crashlog.thread_exception_data1073 code, subcode = exception_codes_match.groups()1074 self.crashlog.exception["rawCodes"] = [1075 int(code, base=16),1076 int(subcode, base=16),1077 ]1078 else:1079 if "type" not in self.crashlog.exception:1080 return False1081 exception_extra_match = self.exception_extra_regex.search(line)1082 if exception_extra_match:1083 self.crashlog.exception["message"] = exception_extra_match.group(1)1084 return True1085 1086 def parse_normal(self, line):1087 if line.startswith("Process:"):1088 (self.crashlog.process_name, pid_with_brackets) = (1089 line[8:].strip().split(" [")1090 )1091 self.crashlog.process_id = pid_with_brackets.strip("[]")1092 elif line.startswith("Path:"):1093 self.crashlog.process_path = line[5:].strip()1094 elif line.startswith("Identifier:"):1095 self.crashlog.process_identifier = line[11:].strip()1096 elif line.startswith("Version:"):1097 version_string = line[8:].strip()1098 matched_pair = re.search(r"(.+)\((.+)\)", version_string)1099 if matched_pair:1100 self.crashlog.process_version = matched_pair.group(1)1101 self.crashlog.process_compatability_version = matched_pair.group(2)1102 else:1103 self.crashlog.process = version_string1104 self.crashlog.process_compatability_version = version_string1105 elif line.startswith("Code Type:"):1106 if "ARM-64" in line:1107 self.crashlog.process_arch = "arm64"1108 elif "X86-64" in line:1109 self.crashlog.process_arch = "x86_64"1110 elif self.parent_process_regex.search(line):1111 parent_process_match = self.parent_process_regex.search(line)1112 self.crashlog.parent_process_name = parent_process_match.group(1)1113 self.crashlog.parent_process_id = parent_process_match.group(2)1114 elif line.startswith("Exception"):1115 self.parse_exception(line)1116 return1117 elif line.startswith("Crashed Thread:"):1118 self.crashlog.crashed_thread_idx = int(line[15:].strip().split()[0])1119 return1120 elif line.startswith("Triggered by Thread:"): # iOS1121 self.crashlog.crashed_thread_idx = int(line[20:].strip().split()[0])1122 return1123 elif line.startswith("Report Version:"):1124 self.crashlog.version = int(line[15:].strip())1125 return1126 elif line.startswith("System Profile:"):1127 self.parse_mode = self.CrashLogParseMode.SYSTEM1128 return1129 elif (1130 line.startswith("Interval Since Last Report:")1131 or line.startswith("Crashes Since Last Report:")1132 or line.startswith("Per-App Interval Since Last Report:")1133 or line.startswith("Per-App Crashes Since Last Report:")1134 or line.startswith("Sleep/Wake UUID:")1135 or line.startswith("Anonymous UUID:")1136 ):1137 # ignore these1138 return1139 elif line.startswith("Thread"):1140 thread_state_match = self.thread_state_regex.search(line)1141 if thread_state_match:1142 self.app_specific_backtrace = False1143 thread_state_match = self.thread_regex.search(line)1144 if thread_state_match:1145 thread_idx = int(thread_state_match.group(1))1146 else:1147 thread_idx = self.crashlog.crashed_thread_idx1148 self.parse_mode = self.CrashLogParseMode.THREGS1149 self.thread = self.crashlog.threads[thread_idx]1150 return1151 thread_insts_match = self.thread_instrs_regex.search(line)1152 if thread_insts_match:1153 self.parse_mode = self.CrashLogParseMode.INSTRS1154 return1155 thread_match = self.thread_regex.search(line)1156 if thread_match:1157 self.app_specific_backtrace = False1158 self.parse_mode = self.CrashLogParseMode.THREAD1159 thread_idx = int(thread_match.group(1))1160 self.thread = self.crashlog.Thread(1161 thread_idx, False, self.crashlog.process_arch1162 )1163 return1164 return1165 elif line.startswith("Binary Images:"):1166 self.parse_mode = self.CrashLogParseMode.IMAGES1167 return1168 elif line.startswith("Application Specific Backtrace"):1169 app_backtrace_match = self.app_backtrace_regex.search(line)1170 if app_backtrace_match:1171 self.parse_mode = self.CrashLogParseMode.THREAD1172 self.app_specific_backtrace = True1173 idx = int(app_backtrace_match.group(1))1174 self.thread = self.crashlog.Thread(1175 idx, True, self.crashlog.process_arch1176 )1177 self.thread.name = "Application Specific Backtrace"1178 elif line.startswith("Last Exception Backtrace:"): # iOS1179 self.parse_mode = self.CrashLogParseMode.THREAD1180 self.app_specific_backtrace = True1181 idx = 11182 self.thread = self.crashlog.Thread(idx, True, self.crashlog.process_arch)1183 self.thread.name = "Last Exception Backtrace"1184 self.crashlog.info_lines.append(line.strip())1185 1186 def parse_thread(self, line):1187 if line.startswith("Thread"):1188 return False1189 if self.null_frame_regex.search(line):1190 print('warning: thread parser ignored null-frame: "%s"' % line)1191 return False1192 frame_match = self.frame_regex.search(line)1193 if not frame_match:1194 print('error: frame regex failed for line: "%s"' % line)1195 return False1196 1197 frame_id = (1198 frame_img_name1199 ) = (1200 frame_addr1201 ) = frame_symbol = frame_offset = frame_file = frame_line = frame_column = None1202 1203 if len(frame_match.groups()) == 3:1204 # Get the image UUID from the frame image name.1205 (frame_id, frame_img_name, frame_addr) = frame_match.groups()1206 elif len(frame_match.groups()) == 5:1207 (1208 frame_id,1209 frame_img_name,1210 frame_addr,1211 frame_symbol,1212 frame_offset,1213 ) = frame_match.groups()1214 elif len(frame_match.groups()) == 7:1215 (1216 frame_id,1217 frame_img_name,1218 frame_addr,1219 frame_symbol,1220 frame_offset,1221 frame_file,1222 frame_line,1223 ) = frame_match.groups()1224 elif len(frame_match.groups()) == 8:1225 (1226 frame_id,1227 frame_img_name,1228 frame_addr,1229 frame_symbol,1230 frame_offset,1231 frame_file,1232 frame_line,1233 frame_column,1234 ) = frame_match.groups()1235 1236 self.thread.add_ident(frame_img_name)1237 if frame_img_name not in self.crashlog.idents:1238 self.crashlog.idents.append(frame_img_name)1239 1240 description = ""1241 # Since images are parsed after threads, we need to build a1242 # map for every image with a list of all the symbols and addresses1243 if frame_img_name and frame_addr and frame_symbol:1244 description = frame_symbol1245 frame_offset_value = 01246 if frame_offset:1247 description += " + " + frame_offset1248 frame_offset_value = int(frame_offset, 0)1249 if frame_img_name not in self.symbols:1250 self.symbols[frame_img_name] = list()1251 self.symbols[frame_img_name].append(1252 {1253 "name": frame_symbol,1254 "address": int(frame_addr, 0) - frame_offset_value,1255 }1256 )1257 1258 self.thread.frames.append(1259 self.crashlog.Frame(int(frame_id), int(frame_addr, 0), description)1260 )1261 1262 return True1263 1264 def parse_images(self, line):1265 image_match = self.image_regex_uuid.search(line)1266 if image_match:1267 (1268 img_lo,1269 img_hi,1270 img_name,1271 img_version,1272 img_uuid,1273 img_path,1274 ) = image_match.groups()1275 1276 image = self.crashlog.DarwinImage(1277 int(img_lo, 0),1278 int(img_hi, 0),1279 img_name.strip(),1280 img_version.strip() if img_version else "",1281 uuid.UUID(img_uuid),1282 img_path,1283 self.options.verbose,1284 )1285 unqualified_img_name = os.path.basename(img_path)1286 if unqualified_img_name in self.symbols:1287 for symbol in self.symbols[unqualified_img_name]:1288 image.symbols[symbol["name"]] = {1289 "name": symbol["name"],1290 "type": "code",1291 # NOTE: "address" is actually the symbol image offset1292 "address": symbol["address"] - int(img_lo, 0),1293 }1294 1295 self.crashlog.images.append(image)1296 return True1297 else:1298 if self.options.debug:1299 print("error: image regex failed for: %s" % line)1300 return False1301 1302 def parse_thread_registers(self, line):1303 # "r12: 0x00007fff6b5939c8 r13: 0x0000000007000006 r14: 0x0000000000002a03 r15: 0x0000000000000c00"1304 reg_values = re.findall("([a-z0-9]+): (0x[0-9a-f]+)", line, re.I)1305 for reg, value in reg_values:1306 self.thread.registers[reg] = int(value, 16)1307 return len(reg_values) != 01308 1309 def parse_system(self, line):1310 self.crashlog.system_profile.append(line)1311 return True1312 1313 def parse_instructions(self, line):1314 pass1315 1316 1317def save_crashlog(debugger, command, exe_ctx, result, dict):1318 usage = "save_crashlog [options] <output-path>"1319 description = """Export the state of current target into a crashlog file"""1320 parser = argparse.ArgumentParser(1321 description=description,1322 prog="save_crashlog",1323 formatter_class=argparse.ArgumentDefaultsHelpFormatter,1324 )1325 parser.add_argument(1326 "output",1327 metavar="output-file",1328 type=argparse.FileType("w", encoding="utf-8"),1329 nargs=1,1330 )1331 parser.add_argument(1332 "-v",1333 "--verbose",1334 action="store_true",1335 dest="verbose",1336 help="display verbose debug info",1337 default=False,1338 )1339 try:1340 options = parser.parse_args(shlex.split(command))1341 except Exception as e:1342 result.SetError(str(e))1343 return1344 target = exe_ctx.target1345 if target:1346 out_file = options.output[0]1347 identifier = target.executable.basename1348 process = exe_ctx.process1349 if process:1350 pid = process.id1351 if pid != lldb.LLDB_INVALID_PROCESS_ID:1352 out_file.write("Process: %s [%u]\n" % (identifier, pid))1353 out_file.write("Path: %s\n" % (target.executable.fullpath))1354 out_file.write("Identifier: %s\n" % (identifier))1355 out_file.write(1356 "\nDate/Time: %s\n"1357 % (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))1358 )1359 out_file.write(1360 "OS Version: Mac OS X %s (%s)\n"1361 % (1362 platform.mac_ver()[0],1363 subprocess.check_output("sysctl -n kern.osversion", shell=True).decode(1364 "utf-8"1365 ),1366 )1367 )1368 out_file.write("Report Version: 9\n")1369 for thread_idx in range(process.num_threads):1370 thread = process.thread[thread_idx]1371 out_file.write("\nThread %u:\n" % (thread_idx))1372 for frame_idx, frame in enumerate(thread.frames):1373 frame_pc = frame.pc1374 frame_offset = 01375 if frame.function:1376 block = frame.GetFrameBlock()1377 block_range = block.range[frame.addr]1378 if block_range:1379 block_start_addr = block_range[0]1380 frame_offset = frame_pc - block_start_addr.GetLoadAddress(1381 target1382 )1383 else:1384 frame_offset = frame_pc - frame.function.addr.GetLoadAddress(1385 target1386 )1387 elif frame.symbol:1388 frame_offset = frame_pc - frame.symbol.addr.GetLoadAddress(target)1389 out_file.write(1390 "%-3u %-32s 0x%16.16x %s"1391 % (frame_idx, frame.module.file.basename, frame_pc, frame.name)1392 )1393 if frame_offset > 0:1394 out_file.write(" + %u" % (frame_offset))1395 line_entry = frame.line_entry1396 if line_entry:1397 if options.verbose:1398 # This will output the fullpath + line + column1399 out_file.write(" %s" % (line_entry))1400 else:1401 out_file.write(1402 " %s:%u" % (line_entry.file.basename, line_entry.line)1403 )1404 column = line_entry.column1405 if column:1406 out_file.write(":%u" % (column))1407 out_file.write("\n")1408 1409 out_file.write("\nBinary Images:\n")1410 for module in target.modules:1411 text_segment = module.section["__TEXT"]1412 if text_segment:1413 text_segment_load_addr = text_segment.GetLoadAddress(target)1414 if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS:1415 text_segment_end_load_addr = (1416 text_segment_load_addr + text_segment.size1417 )1418 identifier = module.file.basename1419 module_version = "???"1420 module_version_array = module.GetVersion()1421 if module_version_array:1422 module_version = ".".join(map(str, module_version_array))1423 out_file.write(1424 " 0x%16.16x - 0x%16.16x %s (%s - ???) <%s> %s\n"1425 % (1426 text_segment_load_addr,1427 text_segment_end_load_addr,1428 identifier,1429 module_version,1430 module.GetUUIDString(),1431 module.file.fullpath,1432 )1433 )1434 out_file.close()1435 else:1436 result.SetError("invalid target")1437 1438 1439class Symbolicate:1440 def __init__(self, debugger, internal_dict):1441 pass1442 1443 def __call__(self, debugger, command, exe_ctx, result):1444 SymbolicateCrashLogs(debugger, shlex.split(command), result, True)1445 1446 def get_short_help(self):1447 return "Symbolicate one or more darwin crash log files."1448 1449 def get_long_help(self):1450 arg_parser = CrashLogOptionParser()1451 return arg_parser.format_help()1452 1453 1454def SymbolicateCrashLog(crash_log, options):1455 if options.debug:1456 crash_log.dump()1457 if not crash_log.images:1458 print("error: no images in crash log")1459 return1460 1461 if options.dump_image_list:1462 print("Binary Images:")1463 for image in crash_log.images:1464 if options.verbose:1465 print(image.debug_dump())1466 else:1467 print(image)1468 1469 target = crash_log.create_target()1470 if not target:1471 return1472 1473 crash_log.load_images(options)1474 1475 if crash_log.backtraces:1476 for thread in crash_log.backtraces:1477 thread.dump_symbolicated(crash_log, options)1478 print()1479 1480 for thread in crash_log.threads:1481 if options.crashed_only and not (1482 thread.crashed or thread.app_specific_backtrace1483 ):1484 continue1485 thread.dump_symbolicated(crash_log, options)1486 print()1487 1488 if crash_log.errors:1489 print("Errors:")1490 for error in crash_log.errors:1491 print(error)1492 1493 1494def load_crashlog_in_scripted_process(debugger, crashlog_path, options, result):1495 crashlog = CrashLogParser.create(debugger, crashlog_path, options).parse()1496 1497 target = lldb.SBTarget()1498 # 1. Try to use the user-provided target1499 if options.target_path:1500 target = debugger.CreateTarget(options.target_path)1501 if not target:1502 raise InteractiveCrashLogException(1503 "couldn't create target provided by the user (%s)" % options.target_path1504 )1505 crashlog.target = target1506 1507 # 2. If the user didn't provide a target, try to create a target using the symbolicator1508 if not target or not target.IsValid():1509 target = crashlog.create_target()1510 # 3. If that didn't work, create a dummy target1511 if target is None or not target.IsValid():1512 arch = crashlog.process_arch1513 if not arch:1514 raise InteractiveCrashLogException(1515 "couldn't find the architecture to create the target"1516 )1517 target = debugger.CreateTargetWithFileAndArch(None, arch)1518 # 4. Fail1519 if target is None or not target.IsValid():1520 raise InteractiveCrashLogException("couldn't create target")1521 1522 ci = debugger.GetCommandInterpreter()1523 if not ci:1524 raise InteractiveCrashLogException("couldn't get command interpreter")1525 1526 ci.HandleCommand("script from lldb.macosx import crashlog_scripted_process", result)1527 if not result.Succeeded():1528 raise InteractiveCrashLogException(1529 "couldn't import crashlog scripted process module"1530 )1531 1532 structured_data = lldb.SBStructuredData()1533 structured_data.SetFromJSON(1534 json.dumps(1535 {1536 "file_path": crashlog_path,1537 "load_all_images": options.load_all_images,1538 "crashed_only": options.crashed_only,1539 "no_parallel_image_loading": options.no_parallel_image_loading,1540 }1541 )1542 )1543 1544 crashlog_sd = lldb.SBStructuredData()1545 crashlog_sd.SetGenericValue(1546 lldb.SBScriptObject(crashlog, lldb.eScriptLanguagePython)1547 )1548 structured_data.SetValueForKey("crashlog", crashlog_sd)1549 1550 launch_info = lldb.SBLaunchInfo(None)1551 launch_info.SetProcessPluginName("ScriptedProcess")1552 launch_info.SetScriptedProcessClassName(1553 "crashlog_scripted_process.CrashLogScriptedProcess"1554 )1555 launch_info.SetScriptedProcessDictionary(structured_data)1556 1557 error = lldb.SBError()1558 process = target.Launch(launch_info, error)1559 1560 if not process or error.Fail():1561 raise InteractiveCrashLogException("couldn't launch Scripted Process", error)1562 1563 if not options.skip_status:1564 1565 @contextlib.contextmanager1566 def synchronous(debugger):1567 async_state = debugger.GetAsync()1568 debugger.SetAsync(False)1569 try:1570 yield1571 finally:1572 debugger.SetAsync(async_state)1573 1574 with synchronous(debugger):1575 run_options = lldb.SBCommandInterpreterRunOptions()1576 run_options.SetStopOnError(True)1577 run_options.SetStopOnCrash(True)1578 run_options.SetEchoCommands(True)1579 1580 commands_stream = lldb.SBStream()1581 commands_stream.Print("process status --verbose\n")1582 commands_stream.Print("thread backtrace --extended true\n")1583 error = debugger.SetInputString(commands_stream.GetData())1584 if error.Success():1585 debugger.RunCommandInterpreter(True, False, run_options, 0, False, True)1586 1587 1588class CrashLogLoadingMode(str, enum.Enum):1589 batch = "batch"1590 interactive = "interactive"1591 1592 1593def CreateSymbolicateCrashLogOptions(command_name, description):1594 usage = "crashlog [options] <FILE> [FILE ...]"1595 arg_parser = argparse.ArgumentParser(1596 description=description,1597 prog="crashlog",1598 usage=usage,1599 formatter_class=argparse.ArgumentDefaultsHelpFormatter,1600 )1601 arg_parser.add_argument(1602 "reports",1603 metavar="FILE",1604 type=str,1605 nargs="*",1606 help="crash report(s) to symbolicate",1607 )1608 1609 arg_parser.add_argument(1610 "-m",1611 "--mode",1612 choices=[mode.value for mode in CrashLogLoadingMode],1613 help="change how the symbolicated process and threads are displayed to the user (default: interactive)",1614 )1615 arg_parser.add_argument(1616 "--version",1617 "-V",1618 dest="version",1619 action="store_true",1620 help="Show crashlog version",1621 default=False,1622 )1623 arg_parser.add_argument(1624 "--verbose",1625 "-v",1626 action="store_true",1627 dest="verbose",1628 help="display verbose debug info",1629 default=False,1630 )1631 arg_parser.add_argument(1632 "--debug",1633 "-g",1634 action="store_true",1635 dest="debug",1636 help="display verbose debug logging",1637 default=False,1638 )1639 arg_parser.add_argument(1640 "--load-all",1641 "-a",1642 action="store_true",1643 dest="load_all_images",1644 help="load all executable images, not just the images found in the "1645 "crashed stack frames, loads stackframes for all the threads in "1646 "interactive mode.",1647 default=False,1648 )1649 arg_parser.add_argument(1650 "--images",1651 action="store_true",1652 dest="dump_image_list",1653 help="show image list",1654 default=False,1655 )1656 arg_parser.add_argument(1657 "--debug-delay",1658 type=int,1659 dest="debug_delay",1660 metavar="NSEC",1661 help="pause for NSEC seconds for debugger",1662 default=0,1663 )1664 # NOTE: Requires python 3.91665 # arg_parser.add_argument(1666 # "--crashed-only",1667 # "-c",1668 # action=argparse.BooleanOptionalAction,1669 # dest="crashed_only",1670 # help="only symbolicate the crashed thread",1671 # default=True,1672 # )1673 arg_parser.add_argument(1674 "--crashed-only",1675 "-c",1676 action="store_true",1677 dest="crashed_only",1678 help="only symbolicate the crashed thread",1679 default=True,1680 )1681 arg_parser.add_argument(1682 "--no-crashed-only",1683 action="store_false",1684 dest="crashed_only",1685 help="in batch mode, symbolicate all threads, not only the crashed one",1686 default=False,1687 )1688 arg_parser.add_argument(1689 "--disasm-depth",1690 "-d",1691 type=int,1692 dest="disassemble_depth",1693 help="set the depth in stack frames that should be disassembled",1694 default=1,1695 )1696 arg_parser.add_argument(1697 "--disasm-all",1698 "-D",1699 action="store_true",1700 dest="disassemble_all_threads",1701 help="enabled disassembly of frames on all threads (not just the crashed thread)",1702 default=False,1703 )1704 arg_parser.add_argument(1705 "--disasm-before",1706 "-B",1707 type=int,1708 dest="disassemble_before",1709 help="the number of instructions to disassemble before the frame PC",1710 default=4,1711 )1712 arg_parser.add_argument(1713 "--disasm-after",1714 "-A",1715 type=int,1716 dest="disassemble_after",1717 help="the number of instructions to disassemble after the frame PC",1718 default=4,1719 )1720 arg_parser.add_argument(1721 "--source-context",1722 "-C",1723 type=int,1724 metavar="NLINES",1725 dest="source_context",1726 help="show NLINES source lines of source context",1727 default=4,1728 )1729 arg_parser.add_argument(1730 "--source-frames",1731 type=int,1732 metavar="NFRAMES",1733 dest="source_frames",1734 help="show source for NFRAMES",1735 default=4,1736 )1737 arg_parser.add_argument(1738 "--source-all",1739 action="store_true",1740 dest="source_all",1741 help="show source for all threads, not just the crashed thread",1742 default=False,1743 )1744 arg_parser.add_argument(1745 "--no-parallel-image-loading",1746 dest="no_parallel_image_loading",1747 action="store_true",1748 help=argparse.SUPPRESS,1749 default=False,1750 )1751 arg_parser.add_argument(1752 "--target",1753 "-t",1754 dest="target_path",1755 help="the target binary path that should be used for interactive crashlog (optional)",1756 default=None,1757 )1758 arg_parser.add_argument(1759 "--skip-status",1760 "-s",1761 dest="skip_status",1762 action="store_true",1763 help="prevent the interactive crashlog to dump the process status and thread backtrace at launch",1764 default=False,1765 )1766 legacy_group = arg_parser.add_mutually_exclusive_group()1767 legacy_group.add_argument(1768 "-i",1769 "--interactive",1770 action="store_true",1771 help=argparse.SUPPRESS,1772 )1773 legacy_group.add_argument(1774 "-b",1775 "--batch",1776 action="store_true",1777 help=argparse.SUPPRESS,1778 )1779 1780 return arg_parser1781 1782 1783def CrashLogOptionParser():1784 description = """Symbolicate one or more darwin crash log files to provide source file and line information,1785inlined stack frames back to the concrete functions, and disassemble the location of the crash1786for the first frame of the crashed thread.1787If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter1788for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been1789created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows1790you to explore the program as if it were stopped at the locations described in the crash log and functions can1791be disassembled and lookups can be performed using the addresses found in the crash log."""1792 return CreateSymbolicateCrashLogOptions("crashlog", description)1793 1794 1795def SymbolicateCrashLogs(debugger, command_args, result, is_command):1796 arg_parser = CrashLogOptionParser()1797 1798 if not len(command_args):1799 arg_parser.print_help()1800 return1801 1802 try:1803 options = arg_parser.parse_args(command_args)1804 except Exception as e:1805 result.SetError(str(e))1806 return1807 1808 # To avoid breaking existing users, we should keep supporting legacy flags1809 # even if we don't use them / advertise them anymore.1810 if not options.mode:1811 if options.batch:1812 options.mode = CrashLogLoadingMode.batch1813 else:1814 options.mode = CrashLogLoadingMode.interactive1815 1816 if options.mode != CrashLogLoadingMode.interactive and (1817 options.target_path or options.skip_status1818 ):1819 print(1820 "Target path (-t) and skipping process status (-s) options can only used in interactive mode (-m=interactive)."1821 )1822 print("Aborting symbolication.")1823 arg_parser.print_help()1824 return1825 1826 if options.version:1827 print(debugger.GetVersionString())1828 return1829 1830 if options.debug:1831 print("command_args = %s" % command_args)1832 print("options", options)1833 print("args", options.reports)1834 1835 # Interactive mode requires running the crashlog command from inside lldb.1836 if options.mode == CrashLogLoadingMode.interactive and not is_command:1837 lldb_exec = (1838 subprocess.check_output(["/usr/bin/xcrun", "-f", "lldb"])1839 .decode("utf-8")1840 .strip()1841 )1842 sys.exit(1843 os.execv(1844 lldb_exec,1845 [1846 lldb_exec,1847 "-o",1848 "command script import lldb.macosx",1849 "-o",1850 "crashlog {}".format(shlex.join(command_args)),1851 ],1852 )1853 )1854 1855 if "NO_PARALLEL_IMG_LOADING" in os.environ:1856 options.no_parallel_image_loading = True1857 1858 if options.version:1859 print(debugger.GetVersionString())1860 return1861 1862 if options.debug_delay > 0:1863 print("Waiting %u seconds for debugger to attach..." % options.debug_delay)1864 time.sleep(options.debug_delay)1865 error = lldb.SBError()1866 1867 def should_run_in_interactive_mode(options, ci):1868 if options.mode == CrashLogLoadingMode.batch:1869 return False1870 elif options.mode == CrashLogLoadingMode.interactive or (1871 ci and ci.IsInteractive()1872 ):1873 return True1874 else:1875 return sys.stdout.isatty()1876 1877 ci = debugger.GetCommandInterpreter()1878 1879 if options.reports:1880 for crashlog_file in options.reports:1881 crashlog_path = os.path.normpath(os.path.expanduser(crashlog_file))1882 if not os.path.exists(crashlog_path):1883 raise FileNotFoundError(1884 "crashlog file %s does not exist" % crashlog_path1885 )1886 if should_run_in_interactive_mode(options, ci):1887 try:1888 load_crashlog_in_scripted_process(1889 debugger, crashlog_path, options, result1890 )1891 except InteractiveCrashLogException as e:1892 result.SetError(str(e))1893 else:1894 crash_log = CrashLogParser.create(1895 debugger, crashlog_path, options1896 ).parse()1897 SymbolicateCrashLog(crash_log, options)1898 1899 1900if __name__ == "__main__":1901 # Create a new debugger instance1902 debugger = lldb.SBDebugger.Create()1903 result = lldb.SBCommandReturnObject()1904 SymbolicateCrashLogs(debugger, sys.argv[1:], result, False)1905 lldb.SBDebugger.Destroy(debugger)1906 1907 1908def __lldb_init_module(debugger, internal_dict):1909 debugger.HandleCommand(1910 "command script add -o -c lldb.macosx.crashlog.Symbolicate -C disk-file crashlog"1911 )1912 debugger.HandleCommand(1913 "command script add -o -f lldb.macosx.crashlog.save_crashlog -C disk-file save_crashlog"1914 )1915 print(1916 '"crashlog" and "save_crashlog" commands have been installed, use '1917 'the "--help" options on these commands for detailed help.'1918 )1919