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 29from __future__ import print_function 30import cmd 31import contextlib 32import datetime 33import glob 34import json 35import optparse 36import os 37import platform 38import plistlib 39import re 40import shlex 41import string 42import subprocess 43import sys 44import time 45import uuid 46 47try: 48 # First try for LLDB in case PYTHONPATH is already correctly setup. 49 import lldb 50except ImportError: 51 # Ask the command line driver for the path to the lldb module. Copy over 52 # the environment so that SDKROOT is propagated to xcrun. 53 command = ['xcrun', 'lldb', '-P'] if platform.system() == 'Darwin' else ['lldb', '-P'] 54 # Extend the PYTHONPATH if the path exists and isn't already there. 55 lldb_python_path = subprocess.check_output(command).decode("utf-8").strip() 56 if os.path.exists(lldb_python_path) and not sys.path.__contains__(lldb_python_path): 57 sys.path.append(lldb_python_path) 58 # Try importing LLDB again. 59 try: 60 import lldb 61 except ImportError: 62 print("error: couldn't locate the 'lldb' module, please set PYTHONPATH correctly") 63 sys.exit(1) 64 65from lldb.utils import symbolication 66 67def read_plist(s): 68 if sys.version_info.major == 3: 69 return plistlib.loads(s) 70 else: 71 return plistlib.readPlistFromString(s) 72 73class CrashLog(symbolication.Symbolicator): 74 class Thread: 75 """Class that represents a thread in a darwin crash log""" 76 77 def __init__(self, index, app_specific_backtrace): 78 self.index = index 79 self.id = index 80 self.frames = list() 81 self.idents = list() 82 self.registers = dict() 83 self.reason = None 84 self.name = None 85 self.queue = None 86 self.crashed = False 87 self.app_specific_backtrace = app_specific_backtrace 88 89 def dump(self, prefix): 90 if self.app_specific_backtrace: 91 print("%Application Specific Backtrace[%u] %s" % (prefix, self.index, self.reason)) 92 else: 93 print("%sThread[%u] %s" % (prefix, self.index, self.reason)) 94 if self.frames: 95 print("%s Frames:" % (prefix)) 96 for frame in self.frames: 97 frame.dump(prefix + ' ') 98 if self.registers: 99 print("%s Registers:" % (prefix)) 100 for reg in self.registers.keys(): 101 print("%s %-8s = %#16.16x" % (prefix, reg, self.registers[reg])) 102 103 def dump_symbolicated(self, crash_log, options): 104 this_thread_crashed = self.app_specific_backtrace 105 if not this_thread_crashed: 106 this_thread_crashed = self.did_crash() 107 if options.crashed_only and this_thread_crashed == False: 108 return 109 110 print("%s" % self) 111 display_frame_idx = -1 112 for frame_idx, frame in enumerate(self.frames): 113 disassemble = ( 114 this_thread_crashed or options.disassemble_all_threads) and frame_idx < options.disassemble_depth 115 if frame_idx == 0: 116 symbolicated_frame_addresses = crash_log.symbolicate( 117 frame.pc & crash_log.addr_mask, options.verbose) 118 else: 119 # Any frame above frame zero and we have to subtract one to 120 # get the previous line entry 121 symbolicated_frame_addresses = crash_log.symbolicate( 122 (frame.pc & crash_log.addr_mask) - 1, options.verbose) 123 124 if symbolicated_frame_addresses: 125 symbolicated_frame_address_idx = 0 126 for symbolicated_frame_address in symbolicated_frame_addresses: 127 display_frame_idx += 1 128 print('[%3u] %s' % (frame_idx, symbolicated_frame_address)) 129 if (options.source_all or self.did_crash( 130 )) and display_frame_idx < options.source_frames and options.source_context: 131 source_context = options.source_context 132 line_entry = symbolicated_frame_address.get_symbol_context().line_entry 133 if line_entry.IsValid(): 134 strm = lldb.SBStream() 135 if line_entry: 136 crash_log.debugger.GetSourceManager().DisplaySourceLinesWithLineNumbers( 137 line_entry.file, line_entry.line, source_context, source_context, "->", strm) 138 source_text = strm.GetData() 139 if source_text: 140 # Indent the source a bit 141 indent_str = ' ' 142 join_str = '\n' + indent_str 143 print('%s%s' % (indent_str, join_str.join(source_text.split('\n')))) 144 if symbolicated_frame_address_idx == 0: 145 if disassemble: 146 instructions = symbolicated_frame_address.get_instructions() 147 if instructions: 148 print() 149 symbolication.disassemble_instructions( 150 crash_log.get_target(), 151 instructions, 152 frame.pc, 153 options.disassemble_before, 154 options.disassemble_after, 155 frame.index > 0) 156 print() 157 symbolicated_frame_address_idx += 1 158 else: 159 print(frame) 160 if self.registers: 161 print() 162 for reg in self.registers.keys(): 163 print(" %-8s = %#16.16x" % (reg, self.registers[reg])) 164 elif self.crashed: 165 print() 166 print("No thread state (register information) available") 167 168 def add_ident(self, ident): 169 if ident not in self.idents: 170 self.idents.append(ident) 171 172 def did_crash(self): 173 return self.reason is not None 174 175 def __str__(self): 176 if self.app_specific_backtrace: 177 s = "Application Specific Backtrace[%u]" % self.index 178 else: 179 s = "Thread[%u]" % self.index 180 if self.reason: 181 s += ' %s' % self.reason 182 return s 183 184 class Frame: 185 """Class that represents a stack frame in a thread in a darwin crash log""" 186 187 def __init__(self, index, pc, description): 188 self.pc = pc 189 self.description = description 190 self.index = index 191 192 def __str__(self): 193 if self.description: 194 return "[%3u] 0x%16.16x %s" % ( 195 self.index, self.pc, self.description) 196 else: 197 return "[%3u] 0x%16.16x" % (self.index, self.pc) 198 199 def dump(self, prefix): 200 print("%s%s" % (prefix, str(self))) 201 202 class DarwinImage(symbolication.Image): 203 """Class that represents a binary images in a darwin crash log""" 204 dsymForUUIDBinary = '/usr/local/bin/dsymForUUID' 205 if not os.path.exists(dsymForUUIDBinary): 206 try: 207 dsymForUUIDBinary = subprocess.check_output('which dsymForUUID', 208 shell=True).decode("utf-8").rstrip('\n') 209 except: 210 dsymForUUIDBinary = "" 211 212 dwarfdump_uuid_regex = re.compile( 213 'UUID: ([-0-9a-fA-F]+) \(([^\(]+)\) .*') 214 215 def __init__( 216 self, 217 text_addr_lo, 218 text_addr_hi, 219 identifier, 220 version, 221 uuid, 222 path, 223 verbose): 224 symbolication.Image.__init__(self, path, uuid) 225 self.add_section( 226 symbolication.Section( 227 text_addr_lo, 228 text_addr_hi, 229 "__TEXT")) 230 self.identifier = identifier 231 self.version = version 232 self.verbose = verbose 233 234 def show_symbol_progress(self): 235 """ 236 Hide progress output and errors from system frameworks as they are plentiful. 237 """ 238 if self.verbose: 239 return True 240 return not (self.path.startswith("/System/Library/") or 241 self.path.startswith("/usr/lib/")) 242 243 244 def find_matching_slice(self): 245 dwarfdump_cmd_output = subprocess.check_output( 246 'dwarfdump --uuid "%s"' % self.path, shell=True).decode("utf-8") 247 self_uuid = self.get_uuid() 248 for line in dwarfdump_cmd_output.splitlines(): 249 match = self.dwarfdump_uuid_regex.search(line) 250 if match: 251 dwarf_uuid_str = match.group(1) 252 dwarf_uuid = uuid.UUID(dwarf_uuid_str) 253 if self_uuid == dwarf_uuid: 254 self.resolved_path = self.path 255 self.arch = match.group(2) 256 return True 257 if not self.resolved_path: 258 self.unavailable = True 259 if self.show_symbol_progress(): 260 print(("error\n error: unable to locate '%s' with UUID %s" 261 % (self.path, self.get_normalized_uuid_string()))) 262 return False 263 264 def locate_module_and_debug_symbols(self): 265 # Don't load a module twice... 266 if self.resolved: 267 return True 268 # Mark this as resolved so we don't keep trying 269 self.resolved = True 270 uuid_str = self.get_normalized_uuid_string() 271 if self.show_symbol_progress(): 272 print('Getting symbols for %s %s...\n' % (uuid_str, self.path), end=' ') 273 if os.path.exists(self.dsymForUUIDBinary): 274 dsym_for_uuid_command = '%s %s' % ( 275 self.dsymForUUIDBinary, uuid_str) 276 s = subprocess.check_output(dsym_for_uuid_command, shell=True) 277 if s: 278 try: 279 plist_root = read_plist(s) 280 except: 281 print(("Got exception: ", sys.exc_info()[1], " handling dsymForUUID output: \n", s)) 282 raise 283 if plist_root: 284 plist = plist_root[uuid_str] 285 if plist: 286 if 'DBGArchitecture' in plist: 287 self.arch = plist['DBGArchitecture'] 288 if 'DBGDSYMPath' in plist: 289 self.symfile = os.path.realpath( 290 plist['DBGDSYMPath']) 291 if 'DBGSymbolRichExecutable' in plist: 292 self.path = os.path.expanduser( 293 plist['DBGSymbolRichExecutable']) 294 self.resolved_path = self.path 295 if not self.resolved_path and os.path.exists(self.path): 296 if not self.find_matching_slice(): 297 return False 298 if not self.resolved_path and not os.path.exists(self.path): 299 try: 300 mdfind_results = subprocess.check_output( 301 ["/usr/bin/mdfind", 302 "com_apple_xcode_dsym_uuids == %s" % uuid_str]).decode("utf-8").splitlines() 303 found_matching_slice = False 304 for dsym in mdfind_results: 305 dwarf_dir = os.path.join(dsym, 'Contents/Resources/DWARF') 306 if not os.path.exists(dwarf_dir): 307 # Not a dSYM bundle, probably an Xcode archive. 308 continue 309 print('falling back to binary inside "%s"' % dsym) 310 self.symfile = dsym 311 for filename in os.listdir(dwarf_dir): 312 self.path = os.path.join(dwarf_dir, filename) 313 if self.find_matching_slice(): 314 found_matching_slice = True 315 break 316 if found_matching_slice: 317 break 318 except: 319 pass 320 if (self.resolved_path and os.path.exists(self.resolved_path)) or ( 321 self.path and os.path.exists(self.path)): 322 print('Resolved symbols for %s %s...\n' % (uuid_str, self.path), end=' ') 323 return True 324 else: 325 self.unavailable = True 326 return False 327 328 def __init__(self, debugger, path, verbose): 329 """CrashLog constructor that take a path to a darwin crash log file""" 330 symbolication.Symbolicator.__init__(self, debugger) 331 self.path = os.path.expanduser(path) 332 self.info_lines = list() 333 self.system_profile = list() 334 self.threads = list() 335 self.backtraces = list() # For application specific backtraces 336 self.idents = list() # A list of the required identifiers for doing all stack backtraces 337 self.errors = list() 338 self.crashed_thread_idx = -1 339 self.version = -1 340 self.target = None 341 self.verbose = verbose 342 343 def dump(self): 344 print("Crash Log File: %s" % (self.path)) 345 if self.backtraces: 346 print("\nApplication Specific Backtraces:") 347 for thread in self.backtraces: 348 thread.dump(' ') 349 print("\nThreads:") 350 for thread in self.threads: 351 thread.dump(' ') 352 print("\nImages:") 353 for image in self.images: 354 image.dump(' ') 355 356 def set_main_image(self, identifier): 357 for i, image in enumerate(self.images): 358 if image.identifier == identifier: 359 self.images.insert(0, self.images.pop(i)) 360 break 361 362 def find_image_with_identifier(self, identifier): 363 for image in self.images: 364 if image.identifier == identifier: 365 return image 366 regex_text = '^.*\.%s$' % (re.escape(identifier)) 367 regex = re.compile(regex_text) 368 for image in self.images: 369 if regex.match(image.identifier): 370 return image 371 return None 372 373 def create_target(self): 374 if self.target is None: 375 self.target = symbolication.Symbolicator.create_target(self) 376 if self.target: 377 return self.target 378 # We weren't able to open the main executable as, but we can still 379 # symbolicate 380 print('crashlog.create_target()...2') 381 if self.idents: 382 for ident in self.idents: 383 image = self.find_image_with_identifier(ident) 384 if image: 385 self.target = image.create_target(self.debugger) 386 if self.target: 387 return self.target # success 388 print('crashlog.create_target()...3') 389 for image in self.images: 390 self.target = image.create_target(self.debugger) 391 if self.target: 392 return self.target # success 393 print('crashlog.create_target()...4') 394 print('error: Unable to locate any executables from the crash log.') 395 print(' Try loading the executable into lldb before running crashlog') 396 print(' and/or make sure the .dSYM bundles can be found by Spotlight.') 397 return self.target 398 399 def get_target(self): 400 return self.target 401 402 403class CrashLogFormatException(Exception): 404 pass 405 406 407class CrashLogParseException(Exception): 408 pass 409 410 411class CrashLogParser: 412 def parse(self, debugger, path, verbose): 413 try: 414 return JSONCrashLogParser(debugger, path, verbose).parse() 415 except CrashLogFormatException: 416 return TextCrashLogParser(debugger, path, verbose).parse() 417 418 419class JSONCrashLogParser: 420 def __init__(self, debugger, path, verbose): 421 self.path = os.path.expanduser(path) 422 self.verbose = verbose 423 self.crashlog = CrashLog(debugger, self.path, self.verbose) 424 425 def parse_json(self, buffer): 426 try: 427 return json.loads(buffer) 428 except: 429 # The first line can contain meta data. Try stripping it and try 430 # again. 431 head, _, tail = buffer.partition('\n') 432 return json.loads(tail) 433 434 def parse(self): 435 with open(self.path, 'r') as f: 436 buffer = f.read() 437 438 try: 439 self.data = self.parse_json(buffer) 440 except: 441 raise CrashLogFormatException() 442 443 try: 444 self.parse_process_info(self.data) 445 self.parse_images(self.data['usedImages']) 446 self.parse_main_image(self.data) 447 self.parse_threads(self.data['threads']) 448 self.parse_errors(self.data) 449 thread = self.crashlog.threads[self.crashlog.crashed_thread_idx] 450 reason = self.parse_crash_reason(self.data['exception']) 451 if thread.reason: 452 thread.reason = '{} {}'.format(thread.reason, reason) 453 else: 454 thread.reason = reason 455 except (KeyError, ValueError, TypeError) as e: 456 raise CrashLogParseException( 457 'Failed to parse JSON crashlog: {}: {}'.format( 458 type(e).__name__, e)) 459 460 return self.crashlog 461 462 def get_used_image(self, idx): 463 return self.data['usedImages'][idx] 464 465 def parse_process_info(self, json_data): 466 self.crashlog.process_id = json_data['pid'] 467 self.crashlog.process_identifier = json_data['procName'] 468 self.crashlog.process_path = json_data['procPath'] 469 470 def parse_crash_reason(self, json_exception): 471 exception_type = json_exception['type'] 472 exception_signal = " " 473 if 'signal' in json_exception: 474 exception_signal += "({})".format(json_exception['signal']) 475 476 if 'codes' in json_exception: 477 exception_extra = " ({})".format(json_exception['codes']) 478 elif 'subtype' in json_exception: 479 exception_extra = " ({})".format(json_exception['subtype']) 480 else: 481 exception_extra = "" 482 return "{}{}{}".format(exception_type, exception_signal, 483 exception_extra) 484 485 def parse_images(self, json_images): 486 idx = 0 487 for json_image in json_images: 488 img_uuid = uuid.UUID(json_image['uuid']) 489 low = int(json_image['base']) 490 high = int(0) 491 name = json_image['name'] if 'name' in json_image else '' 492 path = json_image['path'] if 'path' in json_image else '' 493 version = '' 494 darwin_image = self.crashlog.DarwinImage(low, high, name, version, 495 img_uuid, path, 496 self.verbose) 497 self.crashlog.images.append(darwin_image) 498 idx += 1 499 500 def parse_main_image(self, json_data): 501 if 'procName' in json_data: 502 proc_name = json_data['procName'] 503 self.crashlog.set_main_image(proc_name) 504 505 def parse_frames(self, thread, json_frames): 506 idx = 0 507 for json_frame in json_frames: 508 image_id = int(json_frame['imageIndex']) 509 json_image = self.get_used_image(image_id) 510 ident = json_image['name'] if 'name' in json_image else '' 511 thread.add_ident(ident) 512 if ident not in self.crashlog.idents: 513 self.crashlog.idents.append(ident) 514 515 frame_offset = int(json_frame['imageOffset']) 516 image_addr = self.get_used_image(image_id)['base'] 517 pc = image_addr + frame_offset 518 thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset)) 519 idx += 1 520 521 def parse_threads(self, json_threads): 522 idx = 0 523 for json_thread in json_threads: 524 thread = self.crashlog.Thread(idx, False) 525 if 'name' in json_thread: 526 thread.name = json_thread['name'] 527 thread.reason = json_thread['name'] 528 if 'id' in json_thread: 529 thread.id = int(json_thread['id']) 530 if json_thread.get('triggered', False): 531 self.crashlog.crashed_thread_idx = idx 532 thread.crashed = True 533 if 'threadState' in json_thread: 534 thread.registers = self.parse_thread_registers( 535 json_thread['threadState']) 536 if 'queue' in json_thread: 537 thread.queue = json_thread.get('queue') 538 self.parse_frames(thread, json_thread.get('frames', [])) 539 self.crashlog.threads.append(thread) 540 idx += 1 541 542 def parse_thread_registers(self, json_thread_state, prefix=None): 543 registers = dict() 544 for key, state in json_thread_state.items(): 545 if key == "rosetta": 546 registers.update(self.parse_thread_registers(state)) 547 continue 548 if key == "x": 549 gpr_dict = { str(idx) : reg for idx,reg in enumerate(state) } 550 registers.update(self.parse_thread_registers(gpr_dict, key)) 551 continue 552 try: 553 value = int(state['value']) 554 registers["{}{}".format(prefix,key)] = value 555 except (KeyError, ValueError, TypeError): 556 pass 557 return registers 558 559 def parse_errors(self, json_data): 560 if 'reportNotes' in json_data: 561 self.crashlog.errors = json_data['reportNotes'] 562 563 564class CrashLogParseMode: 565 NORMAL = 0 566 THREAD = 1 567 IMAGES = 2 568 THREGS = 3 569 SYSTEM = 4 570 INSTRS = 5 571 572 573class TextCrashLogParser: 574 parent_process_regex = re.compile('^Parent Process:\s*(.*)\[(\d+)\]') 575 thread_state_regex = re.compile('^Thread ([0-9]+) crashed with') 576 thread_instrs_regex = re.compile('^Thread ([0-9]+) instruction stream') 577 thread_regex = re.compile('^Thread ([0-9]+)([^:]*):(.*)') 578 app_backtrace_regex = re.compile('^Application Specific Backtrace ([0-9]+)([^:]*):(.*)') 579 version = r'(\(.+\)|(arm|x86_)[0-9a-z]+)\s+' 580 frame_regex = re.compile(r'^([0-9]+)' r'\s' # id 581 r'+(.+?)' r'\s+' # img_name 582 r'(' +version+ r')?' # img_version 583 r'(0x[0-9a-fA-F]{7}[0-9a-fA-F]+)' # addr 584 r' +(.*)' # offs 585 ) 586 null_frame_regex = re.compile(r'^([0-9]+)\s+\?\?\?\s+(0{7}0+) +(.*)') 587 image_regex_uuid = re.compile(r'(0x[0-9a-fA-F]+)' # img_lo 588 r'\s+' '-' r'\s+' # - 589 r'(0x[0-9a-fA-F]+)' r'\s+' # img_hi 590 r'[+]?(.+?)' r'\s+' # img_name 591 r'(' +version+ ')?' # img_version 592 r'(<([-0-9a-fA-F]+)>\s+)?' # img_uuid 593 r'(/.*)' # img_path 594 ) 595 596 597 def __init__(self, debugger, path, verbose): 598 self.path = os.path.expanduser(path) 599 self.verbose = verbose 600 self.thread = None 601 self.app_specific_backtrace = False 602 self.crashlog = CrashLog(debugger, self.path, self.verbose) 603 self.parse_mode = CrashLogParseMode.NORMAL 604 self.parsers = { 605 CrashLogParseMode.NORMAL : self.parse_normal, 606 CrashLogParseMode.THREAD : self.parse_thread, 607 CrashLogParseMode.IMAGES : self.parse_images, 608 CrashLogParseMode.THREGS : self.parse_thread_registers, 609 CrashLogParseMode.SYSTEM : self.parse_system, 610 CrashLogParseMode.INSTRS : self.parse_instructions, 611 } 612 613 def parse(self): 614 with open(self.path,'r') as f: 615 lines = f.read().splitlines() 616 617 for line in lines: 618 line_len = len(line) 619 if line_len == 0: 620 if self.thread: 621 if self.parse_mode == CrashLogParseMode.THREAD: 622 if self.thread.index == self.crashlog.crashed_thread_idx: 623 self.thread.reason = '' 624 if self.crashlog.thread_exception: 625 self.thread.reason += self.crashlog.thread_exception 626 if self.crashlog.thread_exception_data: 627 self.thread.reason += " (%s)" % self.crashlog.thread_exception_data 628 if self.app_specific_backtrace: 629 self.crashlog.backtraces.append(self.thread) 630 else: 631 self.crashlog.threads.append(self.thread) 632 self.thread = None 633 else: 634 # only append an extra empty line if the previous line 635 # in the info_lines wasn't empty 636 if len(self.crashlog.info_lines) > 0 and len(self.crashlog.info_lines[-1]): 637 self.crashlog.info_lines.append(line) 638 self.parse_mode = CrashLogParseMode.NORMAL 639 else: 640 self.parsers[self.parse_mode](line) 641 642 return self.crashlog 643 644 645 def parse_normal(self, line): 646 if line.startswith('Process:'): 647 (self.crashlog.process_name, pid_with_brackets) = line[ 648 8:].strip().split(' [') 649 self.crashlog.process_id = pid_with_brackets.strip('[]') 650 elif line.startswith('Path:'): 651 self.crashlog.process_path = line[5:].strip() 652 elif line.startswith('Identifier:'): 653 self.crashlog.process_identifier = line[11:].strip() 654 elif line.startswith('Version:'): 655 version_string = line[8:].strip() 656 matched_pair = re.search("(.+)\((.+)\)", version_string) 657 if matched_pair: 658 self.crashlog.process_version = matched_pair.group(1) 659 self.crashlog.process_compatability_version = matched_pair.group( 660 2) 661 else: 662 self.crashlog.process = version_string 663 self.crashlog.process_compatability_version = version_string 664 elif self.parent_process_regex.search(line): 665 parent_process_match = self.parent_process_regex.search( 666 line) 667 self.crashlog.parent_process_name = parent_process_match.group(1) 668 self.crashlog.parent_process_id = parent_process_match.group(2) 669 elif line.startswith('Exception Type:'): 670 self.crashlog.thread_exception = line[15:].strip() 671 return 672 elif line.startswith('Exception Codes:'): 673 self.crashlog.thread_exception_data = line[16:].strip() 674 return 675 elif line.startswith('Exception Subtype:'): # iOS 676 self.crashlog.thread_exception_data = line[18:].strip() 677 return 678 elif line.startswith('Crashed Thread:'): 679 self.crashlog.crashed_thread_idx = int(line[15:].strip().split()[0]) 680 return 681 elif line.startswith('Triggered by Thread:'): # iOS 682 self.crashlog.crashed_thread_idx = int(line[20:].strip().split()[0]) 683 return 684 elif line.startswith('Report Version:'): 685 self.crashlog.version = int(line[15:].strip()) 686 return 687 elif line.startswith('System Profile:'): 688 self.parse_mode = CrashLogParseMode.SYSTEM 689 return 690 elif (line.startswith('Interval Since Last Report:') or 691 line.startswith('Crashes Since Last Report:') or 692 line.startswith('Per-App Interval Since Last Report:') or 693 line.startswith('Per-App Crashes Since Last Report:') or 694 line.startswith('Sleep/Wake UUID:') or 695 line.startswith('Anonymous UUID:')): 696 # ignore these 697 return 698 elif line.startswith('Thread'): 699 thread_state_match = self.thread_state_regex.search(line) 700 if thread_state_match: 701 self.app_specific_backtrace = False 702 thread_state_match = self.thread_regex.search(line) 703 thread_idx = int(thread_state_match.group(1)) 704 self.parse_mode = CrashLogParseMode.THREGS 705 self.thread = self.crashlog.threads[thread_idx] 706 return 707 thread_insts_match = self.thread_instrs_regex.search(line) 708 if thread_insts_match: 709 self.parse_mode = CrashLogParseMode.INSTRS 710 return 711 thread_match = self.thread_regex.search(line) 712 if thread_match: 713 self.app_specific_backtrace = False 714 self.parse_mode = CrashLogParseMode.THREAD 715 thread_idx = int(thread_match.group(1)) 716 self.thread = self.crashlog.Thread(thread_idx, False) 717 return 718 return 719 elif line.startswith('Binary Images:'): 720 self.parse_mode = CrashLogParseMode.IMAGES 721 return 722 elif line.startswith('Application Specific Backtrace'): 723 app_backtrace_match = self.app_backtrace_regex.search(line) 724 if app_backtrace_match: 725 self.parse_mode = CrashLogParseMode.THREAD 726 self.app_specific_backtrace = True 727 idx = int(app_backtrace_match.group(1)) 728 self.thread = self.crashlog.Thread(idx, True) 729 elif line.startswith('Last Exception Backtrace:'): # iOS 730 self.parse_mode = CrashLogParseMode.THREAD 731 self.app_specific_backtrace = True 732 idx = 1 733 self.thread = self.crashlog.Thread(idx, True) 734 self.crashlog.info_lines.append(line.strip()) 735 736 def parse_thread(self, line): 737 if line.startswith('Thread'): 738 return 739 if self.null_frame_regex.search(line): 740 print('warning: thread parser ignored null-frame: "%s"' % line) 741 return 742 frame_match = self.frame_regex.search(line) 743 if frame_match: 744 (frame_id, frame_img_name, _, frame_img_version, _, 745 frame_addr, frame_ofs) = frame_match.groups() 746 ident = frame_img_name 747 self.thread.add_ident(ident) 748 if ident not in self.crashlog.idents: 749 self.crashlog.idents.append(ident) 750 self.thread.frames.append(self.crashlog.Frame(int(frame_id), int( 751 frame_addr, 0), frame_ofs)) 752 else: 753 print('error: frame regex failed for line: "%s"' % line) 754 755 def parse_images(self, line): 756 image_match = self.image_regex_uuid.search(line) 757 if image_match: 758 (img_lo, img_hi, img_name, _, img_version, _, 759 _, img_uuid, img_path) = image_match.groups() 760 image = self.crashlog.DarwinImage(int(img_lo, 0), int(img_hi, 0), 761 img_name.strip(), 762 img_version.strip() 763 if img_version else "", 764 uuid.UUID(img_uuid), img_path, 765 self.verbose) 766 self.crashlog.images.append(image) 767 else: 768 print("error: image regex failed for: %s" % line) 769 770 771 def parse_thread_registers(self, line): 772 stripped_line = line.strip() 773 # "r12: 0x00007fff6b5939c8 r13: 0x0000000007000006 r14: 0x0000000000002a03 r15: 0x0000000000000c00" 774 reg_values = re.findall( 775 '([a-zA-Z0-9]+: 0[Xx][0-9a-fA-F]+) *', stripped_line) 776 for reg_value in reg_values: 777 (reg, value) = reg_value.split(': ') 778 self.thread.registers[reg.strip()] = int(value, 0) 779 780 def parse_system(self, line): 781 self.crashlog.system_profile.append(line) 782 783 def parse_instructions(self, line): 784 pass 785 786 787def usage(): 788 print("Usage: lldb-symbolicate.py [-n name] executable-image") 789 sys.exit(0) 790 791 792def save_crashlog(debugger, command, exe_ctx, result, dict): 793 usage = "usage: %prog [options] <output-path>" 794 description = '''Export the state of current target into a crashlog file''' 795 parser = optparse.OptionParser( 796 description=description, 797 prog='save_crashlog', 798 usage=usage) 799 parser.add_option( 800 '-v', 801 '--verbose', 802 action='store_true', 803 dest='verbose', 804 help='display verbose debug info', 805 default=False) 806 try: 807 (options, args) = parser.parse_args(shlex.split(command)) 808 except: 809 result.PutCString("error: invalid options") 810 return 811 if len(args) != 1: 812 result.PutCString( 813 "error: invalid arguments, a single output file is the only valid argument") 814 return 815 out_file = open(args[0], 'w') 816 if not out_file: 817 result.PutCString( 818 "error: failed to open file '%s' for writing...", 819 args[0]) 820 return 821 target = exe_ctx.target 822 if target: 823 identifier = target.executable.basename 824 process = exe_ctx.process 825 if process: 826 pid = process.id 827 if pid != lldb.LLDB_INVALID_PROCESS_ID: 828 out_file.write( 829 'Process: %s [%u]\n' % 830 (identifier, pid)) 831 out_file.write('Path: %s\n' % (target.executable.fullpath)) 832 out_file.write('Identifier: %s\n' % (identifier)) 833 out_file.write('\nDate/Time: %s\n' % 834 (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) 835 out_file.write( 836 'OS Version: Mac OS X %s (%s)\n' % 837 (platform.mac_ver()[0], subprocess.check_output('sysctl -n kern.osversion', shell=True).decode("utf-8"))) 838 out_file.write('Report Version: 9\n') 839 for thread_idx in range(process.num_threads): 840 thread = process.thread[thread_idx] 841 out_file.write('\nThread %u:\n' % (thread_idx)) 842 for (frame_idx, frame) in enumerate(thread.frames): 843 frame_pc = frame.pc 844 frame_offset = 0 845 if frame.function: 846 block = frame.GetFrameBlock() 847 block_range = block.range[frame.addr] 848 if block_range: 849 block_start_addr = block_range[0] 850 frame_offset = frame_pc - block_start_addr.GetLoadAddress(target) 851 else: 852 frame_offset = frame_pc - frame.function.addr.GetLoadAddress(target) 853 elif frame.symbol: 854 frame_offset = frame_pc - frame.symbol.addr.GetLoadAddress(target) 855 out_file.write( 856 '%-3u %-32s 0x%16.16x %s' % 857 (frame_idx, frame.module.file.basename, frame_pc, frame.name)) 858 if frame_offset > 0: 859 out_file.write(' + %u' % (frame_offset)) 860 line_entry = frame.line_entry 861 if line_entry: 862 if options.verbose: 863 # This will output the fullpath + line + column 864 out_file.write(' %s' % (line_entry)) 865 else: 866 out_file.write( 867 ' %s:%u' % 868 (line_entry.file.basename, line_entry.line)) 869 column = line_entry.column 870 if column: 871 out_file.write(':%u' % (column)) 872 out_file.write('\n') 873 874 out_file.write('\nBinary Images:\n') 875 for module in target.modules: 876 text_segment = module.section['__TEXT'] 877 if text_segment: 878 text_segment_load_addr = text_segment.GetLoadAddress(target) 879 if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS: 880 text_segment_end_load_addr = text_segment_load_addr + text_segment.size 881 identifier = module.file.basename 882 module_version = '???' 883 module_version_array = module.GetVersion() 884 if module_version_array: 885 module_version = '.'.join( 886 map(str, module_version_array)) 887 out_file.write( 888 ' 0x%16.16x - 0x%16.16x %s (%s - ???) <%s> %s\n' % 889 (text_segment_load_addr, 890 text_segment_end_load_addr, 891 identifier, 892 module_version, 893 module.GetUUIDString(), 894 module.file.fullpath)) 895 out_file.close() 896 else: 897 result.PutCString("error: invalid target") 898 899 900class Symbolicate: 901 def __init__(self, debugger, internal_dict): 902 pass 903 904 def __call__(self, debugger, command, exe_ctx, result): 905 try: 906 SymbolicateCrashLogs(debugger, shlex.split(command)) 907 except Exception as e: 908 result.PutCString("error: python exception: %s" % e) 909 910 def get_short_help(self): 911 return "Symbolicate one or more darwin crash log files." 912 913 def get_long_help(self): 914 option_parser = CrashLogOptionParser() 915 return option_parser.format_help() 916 917 918def SymbolicateCrashLog(crash_log, options): 919 if options.debug: 920 crash_log.dump() 921 if not crash_log.images: 922 print('error: no images in crash log') 923 return 924 925 if options.dump_image_list: 926 print("Binary Images:") 927 for image in crash_log.images: 928 if options.verbose: 929 print(image.debug_dump()) 930 else: 931 print(image) 932 933 target = crash_log.create_target() 934 if not target: 935 return 936 exe_module = target.GetModuleAtIndex(0) 937 images_to_load = list() 938 loaded_images = list() 939 if options.load_all_images: 940 # --load-all option was specified, load everything up 941 for image in crash_log.images: 942 images_to_load.append(image) 943 else: 944 # Only load the images found in stack frames for the crashed threads 945 if options.crashed_only: 946 for thread in crash_log.threads: 947 if thread.did_crash(): 948 for ident in thread.idents: 949 images = crash_log.find_images_with_identifier(ident) 950 if images: 951 for image in images: 952 images_to_load.append(image) 953 else: 954 print('error: can\'t find image for identifier "%s"' % ident) 955 else: 956 for ident in crash_log.idents: 957 images = crash_log.find_images_with_identifier(ident) 958 if images: 959 for image in images: 960 images_to_load.append(image) 961 else: 962 print('error: can\'t find image for identifier "%s"' % ident) 963 964 for image in images_to_load: 965 if image not in loaded_images: 966 err = image.add_module(target) 967 if err: 968 print(err) 969 else: 970 loaded_images.append(image) 971 972 if crash_log.backtraces: 973 for thread in crash_log.backtraces: 974 thread.dump_symbolicated(crash_log, options) 975 print() 976 977 for thread in crash_log.threads: 978 thread.dump_symbolicated(crash_log, options) 979 print() 980 981 if crash_log.errors: 982 print("Errors:") 983 for error in crash_log.errors: 984 print(error) 985 986def load_crashlog_in_scripted_process(debugger, crash_log_file, options): 987 result = lldb.SBCommandReturnObject() 988 989 crashlog_path = os.path.expanduser(crash_log_file) 990 if not os.path.exists(crashlog_path): 991 result.PutCString("error: crashlog file %s does not exist" % crashlog_path) 992 993 try: 994 crashlog = CrashLogParser().parse(debugger, crashlog_path, False) 995 except Exception as e: 996 result.PutCString("error: python exception: %s" % e) 997 return 998 999 if debugger.GetNumTargets() > 0: 1000 target = debugger.GetTargetAtIndex(0) 1001 else: 1002 target = crashlog.create_target() 1003 if not target: 1004 result.PutCString("error: couldn't create target") 1005 return 1006 1007 ci = debugger.GetCommandInterpreter() 1008 if not ci: 1009 result.PutCString("error: couldn't get command interpreter") 1010 return 1011 1012 res = lldb.SBCommandReturnObject() 1013 ci.HandleCommand('script from lldb.macosx import crashlog_scripted_process', res) 1014 if not res.Succeeded(): 1015 result.PutCString("error: couldn't import crashlog scripted process module") 1016 return 1017 1018 structured_data = lldb.SBStructuredData() 1019 structured_data.SetFromJSON(json.dumps({ "crashlog_path" : crashlog_path, 1020 "load_all_images": options.load_all_images })) 1021 launch_info = lldb.SBLaunchInfo(None) 1022 launch_info.SetProcessPluginName("ScriptedProcess") 1023 launch_info.SetScriptedProcessClassName("crashlog_scripted_process.CrashLogScriptedProcess") 1024 launch_info.SetScriptedProcessDictionary(structured_data) 1025 error = lldb.SBError() 1026 process = target.Launch(launch_info, error) 1027 1028 if not process or error.Fail(): 1029 return 1030 1031 @contextlib.contextmanager 1032 def synchronous(debugger): 1033 async_state = debugger.GetAsync() 1034 debugger.SetAsync(False) 1035 try: 1036 yield 1037 finally: 1038 debugger.SetAsync(async_state) 1039 1040 with synchronous(debugger): 1041 run_options = lldb.SBCommandInterpreterRunOptions() 1042 run_options.SetStopOnError(True) 1043 run_options.SetStopOnCrash(True) 1044 run_options.SetEchoCommands(True) 1045 1046 commands_stream = lldb.SBStream() 1047 commands_stream.Print("process status\n") 1048 commands_stream.Print("thread backtrace\n") 1049 error = debugger.SetInputString(commands_stream.GetData()) 1050 if error.Success(): 1051 debugger.RunCommandInterpreter(True, False, run_options, 0, False, True) 1052 1053def CreateSymbolicateCrashLogOptions( 1054 command_name, 1055 description, 1056 add_interactive_options): 1057 usage = "usage: %prog [options] <FILE> [FILE ...]" 1058 option_parser = optparse.OptionParser( 1059 description=description, prog='crashlog', usage=usage) 1060 option_parser.add_option( 1061 '--verbose', 1062 '-v', 1063 action='store_true', 1064 dest='verbose', 1065 help='display verbose debug info', 1066 default=False) 1067 option_parser.add_option( 1068 '--debug', 1069 '-g', 1070 action='store_true', 1071 dest='debug', 1072 help='display verbose debug logging', 1073 default=False) 1074 option_parser.add_option( 1075 '--load-all', 1076 '-a', 1077 action='store_true', 1078 dest='load_all_images', 1079 help='load all executable images, not just the images found in the ' 1080 'crashed stack frames, loads stackframes for all the threads in ' 1081 'interactive mode.', 1082 default=False) 1083 option_parser.add_option( 1084 '--images', 1085 action='store_true', 1086 dest='dump_image_list', 1087 help='show image list', 1088 default=False) 1089 option_parser.add_option( 1090 '--debug-delay', 1091 type='int', 1092 dest='debug_delay', 1093 metavar='NSEC', 1094 help='pause for NSEC seconds for debugger', 1095 default=0) 1096 option_parser.add_option( 1097 '--crashed-only', 1098 '-c', 1099 action='store_true', 1100 dest='crashed_only', 1101 help='only symbolicate the crashed thread', 1102 default=False) 1103 option_parser.add_option( 1104 '--disasm-depth', 1105 '-d', 1106 type='int', 1107 dest='disassemble_depth', 1108 help='set the depth in stack frames that should be disassembled (default is 1)', 1109 default=1) 1110 option_parser.add_option( 1111 '--disasm-all', 1112 '-D', 1113 action='store_true', 1114 dest='disassemble_all_threads', 1115 help='enabled disassembly of frames on all threads (not just the crashed thread)', 1116 default=False) 1117 option_parser.add_option( 1118 '--disasm-before', 1119 '-B', 1120 type='int', 1121 dest='disassemble_before', 1122 help='the number of instructions to disassemble before the frame PC', 1123 default=4) 1124 option_parser.add_option( 1125 '--disasm-after', 1126 '-A', 1127 type='int', 1128 dest='disassemble_after', 1129 help='the number of instructions to disassemble after the frame PC', 1130 default=4) 1131 option_parser.add_option( 1132 '--source-context', 1133 '-C', 1134 type='int', 1135 metavar='NLINES', 1136 dest='source_context', 1137 help='show NLINES source lines of source context (default = 4)', 1138 default=4) 1139 option_parser.add_option( 1140 '--source-frames', 1141 type='int', 1142 metavar='NFRAMES', 1143 dest='source_frames', 1144 help='show source for NFRAMES (default = 4)', 1145 default=4) 1146 option_parser.add_option( 1147 '--source-all', 1148 action='store_true', 1149 dest='source_all', 1150 help='show source for all threads, not just the crashed thread', 1151 default=False) 1152 if add_interactive_options: 1153 option_parser.add_option( 1154 '-i', 1155 '--interactive', 1156 action='store_true', 1157 help='parse a crash log and load it in a ScriptedProcess', 1158 default=False) 1159 option_parser.add_option( 1160 '-b', 1161 '--batch', 1162 action='store_true', 1163 help='dump symbolicated stackframes without creating a debug session', 1164 default=True) 1165 return option_parser 1166 1167 1168def CrashLogOptionParser(): 1169 description = '''Symbolicate one or more darwin crash log files to provide source file and line information, 1170inlined stack frames back to the concrete functions, and disassemble the location of the crash 1171for the first frame of the crashed thread. 1172If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter 1173for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been 1174created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows 1175you to explore the program as if it were stopped at the locations described in the crash log and functions can 1176be disassembled and lookups can be performed using the addresses found in the crash log.''' 1177 return CreateSymbolicateCrashLogOptions('crashlog', description, True) 1178 1179def SymbolicateCrashLogs(debugger, command_args): 1180 option_parser = CrashLogOptionParser() 1181 try: 1182 (options, args) = option_parser.parse_args(command_args) 1183 except: 1184 return 1185 1186 if options.debug: 1187 print('command_args = %s' % command_args) 1188 print('options', options) 1189 print('args', args) 1190 1191 if options.debug_delay > 0: 1192 print("Waiting %u seconds for debugger to attach..." % options.debug_delay) 1193 time.sleep(options.debug_delay) 1194 error = lldb.SBError() 1195 1196 def should_run_in_interactive_mode(options, ci): 1197 if options.interactive: 1198 return True 1199 elif options.batch: 1200 return False 1201 # elif ci and ci.IsInteractive(): 1202 # return True 1203 else: 1204 return False 1205 1206 ci = debugger.GetCommandInterpreter() 1207 1208 if args: 1209 for crash_log_file in args: 1210 if should_run_in_interactive_mode(options, ci): 1211 load_crashlog_in_scripted_process(debugger, crash_log_file, 1212 options) 1213 else: 1214 crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose) 1215 SymbolicateCrashLog(crash_log, options) 1216 1217if __name__ == '__main__': 1218 # Create a new debugger instance 1219 debugger = lldb.SBDebugger.Create() 1220 SymbolicateCrashLogs(debugger, sys.argv[1:]) 1221 lldb.SBDebugger.Destroy(debugger) 1222 1223def __lldb_init_module(debugger, internal_dict): 1224 debugger.HandleCommand( 1225 'command script add -c lldb.macosx.crashlog.Symbolicate crashlog') 1226 debugger.HandleCommand( 1227 'command script add -f lldb.macosx.crashlog.save_crashlog save_crashlog') 1228 print('"crashlog" and "save_crashlog" commands have been installed, use ' 1229 'the "--help" options on these commands for detailed help.') 1230