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