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