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