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