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