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