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 CrashLogParser: 387 def parse(self, debugger, path, verbose): 388 try: 389 return JSONCrashLogParser(debugger, path, verbose).parse() 390 except CrashLogFormatException: 391 return TextCrashLogParser(debugger, path, verbose).parse() 392 393 394class JSONCrashLogParser: 395 def __init__(self, debugger, path, verbose): 396 self.path = os.path.expanduser(path) 397 self.verbose = verbose 398 self.crashlog = CrashLog(debugger, self.path, self.verbose) 399 400 def parse(self): 401 with open(self.path, 'r') as f: 402 buffer = f.read() 403 404 # First line is meta-data. 405 buffer = buffer[buffer.index('\n') + 1:] 406 407 try: 408 self.data = json.loads(buffer) 409 except ValueError: 410 raise CrashLogFormatException() 411 412 self.parse_process_info(self.data) 413 self.parse_images(self.data['usedImages']) 414 self.parse_threads(self.data['threads']) 415 416 thread = self.crashlog.threads[self.crashlog.crashed_thread_idx] 417 thread.reason = self.parse_crash_reason(self.data['exception']) 418 419 return self.crashlog 420 421 def get_used_image(self, idx): 422 return self.data['usedImages'][idx] 423 424 def parse_process_info(self, json_data): 425 self.crashlog.process_id = json_data['pid'] 426 self.crashlog.process_identifier = json_data['procName'] 427 self.crashlog.process_path = json_data['procPath'] 428 429 def parse_crash_reason(self, json_exception): 430 exception_type = json_exception['type'] 431 exception_signal = json_exception['signal'] 432 if 'codes' in json_exception: 433 exception_extra = " ({})".format(json_exception['codes']) 434 elif 'subtype' in json_exception: 435 exception_extra = " ({})".format(json_exception['subtype']) 436 else: 437 exception_extra = "" 438 return "{} ({}){}".format(exception_type, exception_signal, 439 exception_extra) 440 441 def parse_images(self, json_images): 442 idx = 0 443 for json_image in json_images: 444 img_uuid = uuid.UUID(json_image['uuid']) 445 low = int(json_image['base']) 446 high = int(0) 447 name = json_image['name'] 448 path = json_image['path'] 449 version = "" 450 darwin_image = self.crashlog.DarwinImage(low, high, name, version, 451 img_uuid, path, 452 self.verbose) 453 self.crashlog.images.append(darwin_image) 454 idx += 1 455 456 def parse_frames(self, thread, json_frames): 457 idx = 0 458 for json_frame in json_frames: 459 image_id = int(json_frame['imageIndex']) 460 ident = self.get_used_image(image_id)['name'] 461 thread.add_ident(ident) 462 if ident not in self.crashlog.idents: 463 self.crashlog.idents.append(ident) 464 465 frame_offset = int(json_frame['imageOffset']) 466 image_addr = self.get_used_image(image_id)['base'] 467 pc = image_addr + frame_offset 468 thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset)) 469 idx += 1 470 471 def parse_threads(self, json_threads): 472 idx = 0 473 for json_thread in json_threads: 474 thread = self.crashlog.Thread(idx, False) 475 if json_thread.get('triggered', False): 476 self.crashlog.crashed_thread_idx = idx 477 self.registers = self.parse_thread_registers( 478 json_thread['threadState']) 479 thread.queue = json_thread.get('queue') 480 self.parse_frames(thread, json_thread.get('frames', [])) 481 self.crashlog.threads.append(thread) 482 idx += 1 483 484 def parse_thread_registers(self, json_thread_state): 485 idx = 0 486 registers = dict() 487 for json_reg in json_thread_state.get('x', []): 488 key = str('x{}'.format(idx)) 489 value = int(json_reg['value']) 490 registers[key] = value 491 idx += 1 492 493 for register in ['lr', 'cpsr', 'fp', 'sp', 'esr', 'pc']: 494 if register in json_thread_state: 495 json_reg = json_thread_state[register] 496 registers[register] = int(json_reg['value']) 497 498 return registers 499 500 501class CrashLogParseMode: 502 NORMAL = 0 503 THREAD = 1 504 IMAGES = 2 505 THREGS = 3 506 SYSTEM = 4 507 INSTRS = 5 508 509 510class TextCrashLogParser: 511 parent_process_regex = re.compile('^Parent Process:\s*(.*)\[(\d+)\]') 512 thread_state_regex = re.compile('^Thread ([0-9]+) crashed with') 513 thread_instrs_regex = re.compile('^Thread ([0-9]+) instruction stream') 514 thread_regex = re.compile('^Thread ([0-9]+)([^:]*):(.*)') 515 app_backtrace_regex = re.compile('^Application Specific Backtrace ([0-9]+)([^:]*):(.*)') 516 version = r'(\(.+\)|(arm|x86_)[0-9a-z]+)\s+' 517 frame_regex = re.compile(r'^([0-9]+)' r'\s' # id 518 r'+(.+?)' r'\s+' # img_name 519 r'(' +version+ r')?' # img_version 520 r'(0x[0-9a-fA-F]{7}[0-9a-fA-F]+)' # addr 521 r' +(.*)' # offs 522 ) 523 null_frame_regex = re.compile(r'^([0-9]+)\s+\?\?\?\s+(0{7}0+) +(.*)') 524 image_regex_uuid = re.compile(r'(0x[0-9a-fA-F]+)' # img_lo 525 r'\s+' '-' r'\s+' # - 526 r'(0x[0-9a-fA-F]+)' r'\s+' # img_hi 527 r'[+]?(.+?)' r'\s+' # img_name 528 r'(' +version+ ')?' # img_version 529 r'(<([-0-9a-fA-F]+)>\s+)?' # img_uuid 530 r'(/.*)' # img_path 531 ) 532 533 534 def __init__(self, debugger, path, verbose): 535 self.path = os.path.expanduser(path) 536 self.verbose = verbose 537 self.thread = None 538 self.app_specific_backtrace = False 539 self.crashlog = CrashLog(debugger, self.path, self.verbose) 540 self.parse_mode = CrashLogParseMode.NORMAL 541 self.parsers = { 542 CrashLogParseMode.NORMAL : self.parse_normal, 543 CrashLogParseMode.THREAD : self.parse_thread, 544 CrashLogParseMode.IMAGES : self.parse_images, 545 CrashLogParseMode.THREGS : self.parse_thread_registers, 546 CrashLogParseMode.SYSTEM : self.parse_system, 547 CrashLogParseMode.INSTRS : self.parse_instructions, 548 } 549 550 def parse(self): 551 with open(self.path,'r') as f: 552 lines = f.read().splitlines() 553 554 for line in lines: 555 line_len = len(line) 556 if line_len == 0: 557 if self.thread: 558 if self.parse_mode == CrashLogParseMode.THREAD: 559 if self.thread.index == self.crashlog.crashed_thread_idx: 560 self.thread.reason = '' 561 if self.crashlog.thread_exception: 562 self.thread.reason += self.crashlog.thread_exception 563 if self.crashlog.thread_exception_data: 564 self.thread.reason += " (%s)" % self.crashlog.thread_exception_data 565 if self.app_specific_backtrace: 566 self.crashlog.backtraces.append(self.thread) 567 else: 568 self.crashlog.threads.append(self.thread) 569 self.thread = None 570 else: 571 # only append an extra empty line if the previous line 572 # in the info_lines wasn't empty 573 if len(self.crashlog.info_lines) > 0 and len(self.crashlog.info_lines[-1]): 574 self.crashlog.info_lines.append(line) 575 self.parse_mode = CrashLogParseMode.NORMAL 576 else: 577 self.parsers[self.parse_mode](line) 578 579 return self.crashlog 580 581 582 def parse_normal(self, line): 583 if line.startswith('Process:'): 584 (self.crashlog.process_name, pid_with_brackets) = line[ 585 8:].strip().split(' [') 586 self.crashlog.process_id = pid_with_brackets.strip('[]') 587 elif line.startswith('Path:'): 588 self.crashlog.process_path = line[5:].strip() 589 elif line.startswith('Identifier:'): 590 self.crashlog.process_identifier = line[11:].strip() 591 elif line.startswith('Version:'): 592 version_string = line[8:].strip() 593 matched_pair = re.search("(.+)\((.+)\)", version_string) 594 if matched_pair: 595 self.crashlog.process_version = matched_pair.group(1) 596 self.crashlog.process_compatability_version = matched_pair.group( 597 2) 598 else: 599 self.crashlog.process = version_string 600 self.crashlog.process_compatability_version = version_string 601 elif self.parent_process_regex.search(line): 602 parent_process_match = self.parent_process_regex.search( 603 line) 604 self.crashlog.parent_process_name = parent_process_match.group(1) 605 self.crashlog.parent_process_id = parent_process_match.group(2) 606 elif line.startswith('Exception Type:'): 607 self.crashlog.thread_exception = line[15:].strip() 608 return 609 elif line.startswith('Exception Codes:'): 610 self.crashlog.thread_exception_data = line[16:].strip() 611 return 612 elif line.startswith('Exception Subtype:'): # iOS 613 self.crashlog.thread_exception_data = line[18:].strip() 614 return 615 elif line.startswith('Crashed Thread:'): 616 self.crashlog.crashed_thread_idx = int(line[15:].strip().split()[0]) 617 return 618 elif line.startswith('Triggered by Thread:'): # iOS 619 self.crashlog.crashed_thread_idx = int(line[20:].strip().split()[0]) 620 return 621 elif line.startswith('Report Version:'): 622 self.crashlog.version = int(line[15:].strip()) 623 return 624 elif line.startswith('System Profile:'): 625 self.parse_mode = CrashLogParseMode.SYSTEM 626 return 627 elif (line.startswith('Interval Since Last Report:') or 628 line.startswith('Crashes Since Last Report:') or 629 line.startswith('Per-App Interval Since Last Report:') or 630 line.startswith('Per-App Crashes Since Last Report:') or 631 line.startswith('Sleep/Wake UUID:') or 632 line.startswith('Anonymous UUID:')): 633 # ignore these 634 return 635 elif line.startswith('Thread'): 636 thread_state_match = self.thread_state_regex.search(line) 637 if thread_state_match: 638 self.app_specific_backtrace = False 639 thread_state_match = self.thread_regex.search(line) 640 thread_idx = int(thread_state_match.group(1)) 641 self.parse_mode = CrashLogParseMode.THREGS 642 self.thread = self.crashlog.threads[thread_idx] 643 return 644 thread_insts_match = self.thread_instrs_regex.search(line) 645 if thread_insts_match: 646 self.parse_mode = CrashLogParseMode.INSTRS 647 return 648 thread_match = self.thread_regex.search(line) 649 if thread_match: 650 self.app_specific_backtrace = False 651 self.parse_mode = CrashLogParseMode.THREAD 652 thread_idx = int(thread_match.group(1)) 653 self.thread = self.crashlog.Thread(thread_idx, False) 654 return 655 return 656 elif line.startswith('Binary Images:'): 657 self.parse_mode = CrashLogParseMode.IMAGES 658 return 659 elif line.startswith('Application Specific Backtrace'): 660 app_backtrace_match = self.app_backtrace_regex.search(line) 661 if app_backtrace_match: 662 self.parse_mode = CrashLogParseMode.THREAD 663 self.app_specific_backtrace = True 664 idx = int(app_backtrace_match.group(1)) 665 self.thread = self.crashlog.Thread(idx, True) 666 elif line.startswith('Last Exception Backtrace:'): # iOS 667 self.parse_mode = CrashLogParseMode.THREAD 668 self.app_specific_backtrace = True 669 idx = 1 670 self.thread = self.crashlog.Thread(idx, True) 671 self.crashlog.info_lines.append(line.strip()) 672 673 def parse_thread(self, line): 674 if line.startswith('Thread'): 675 return 676 if self.null_frame_regex.search(line): 677 print('warning: thread parser ignored null-frame: "%s"' % line) 678 return 679 frame_match = self.frame_regex.search(line) 680 if frame_match: 681 (frame_id, frame_img_name, _, frame_img_version, _, 682 frame_addr, frame_ofs) = frame_match.groups() 683 ident = frame_img_name 684 self.thread.add_ident(ident) 685 if ident not in self.crashlog.idents: 686 self.crashlog.idents.append(ident) 687 self.thread.frames.append(self.crashlog.Frame(int(frame_id), int( 688 frame_addr, 0), frame_ofs)) 689 else: 690 print('error: frame regex failed for line: "%s"' % line) 691 692 def parse_images(self, line): 693 image_match = self.image_regex_uuid.search(line) 694 if image_match: 695 (img_lo, img_hi, img_name, _, img_version, _, 696 _, img_uuid, img_path) = image_match.groups() 697 image = self.crashlog.DarwinImage(int(img_lo, 0), int(img_hi, 0), 698 img_name.strip(), 699 img_version.strip() 700 if img_version else "", 701 uuid.UUID(img_uuid), img_path, 702 self.verbose) 703 self.crashlog.images.append(image) 704 else: 705 print("error: image regex failed for: %s" % line) 706 707 708 def parse_thread_registers(self, line): 709 stripped_line = line.strip() 710 # "r12: 0x00007fff6b5939c8 r13: 0x0000000007000006 r14: 0x0000000000002a03 r15: 0x0000000000000c00" 711 reg_values = re.findall( 712 '([a-zA-Z0-9]+: 0[Xx][0-9a-fA-F]+) *', stripped_line) 713 for reg_value in reg_values: 714 (reg, value) = reg_value.split(': ') 715 self.thread.registers[reg.strip()] = int(value, 0) 716 717 def parse_system(self, line): 718 self.crashlog.system_profile.append(line) 719 720 def parse_instructions(self, line): 721 pass 722 723 724def usage(): 725 print("Usage: lldb-symbolicate.py [-n name] executable-image") 726 sys.exit(0) 727 728 729class Interactive(cmd.Cmd): 730 '''Interactive prompt for analyzing one or more Darwin crash logs, type "help" to see a list of supported commands.''' 731 image_option_parser = None 732 733 def __init__(self, crash_logs): 734 cmd.Cmd.__init__(self) 735 self.use_rawinput = False 736 self.intro = 'Interactive crashlogs prompt, type "help" to see a list of supported commands.' 737 self.crash_logs = crash_logs 738 self.prompt = '% ' 739 740 def default(self, line): 741 '''Catch all for unknown command, which will exit the interpreter.''' 742 print("uknown command: %s" % line) 743 return True 744 745 def do_q(self, line): 746 '''Quit command''' 747 return True 748 749 def do_quit(self, line): 750 '''Quit command''' 751 return True 752 753 def do_symbolicate(self, line): 754 description = '''Symbolicate one or more darwin crash log files by index to provide source file and line information, 755 inlined stack frames back to the concrete functions, and disassemble the location of the crash 756 for the first frame of the crashed thread.''' 757 option_parser = CreateSymbolicateCrashLogOptions( 758 'symbolicate', description, False) 759 command_args = shlex.split(line) 760 try: 761 (options, args) = option_parser.parse_args(command_args) 762 except: 763 return 764 765 if args: 766 # We have arguments, they must valid be crash log file indexes 767 for idx_str in args: 768 idx = int(idx_str) 769 if idx < len(self.crash_logs): 770 SymbolicateCrashLog(self.crash_logs[idx], options) 771 else: 772 print('error: crash log index %u is out of range' % (idx)) 773 else: 774 # No arguments, symbolicate all crash logs using the options 775 # provided 776 for idx in range(len(self.crash_logs)): 777 SymbolicateCrashLog(self.crash_logs[idx], options) 778 779 def do_list(self, line=None): 780 '''Dump a list of all crash logs that are currently loaded. 781 782 USAGE: list''' 783 print('%u crash logs are loaded:' % len(self.crash_logs)) 784 for (crash_log_idx, crash_log) in enumerate(self.crash_logs): 785 print('[%u] = %s' % (crash_log_idx, crash_log.path)) 786 787 def do_image(self, line): 788 '''Dump information about one or more binary images in the crash log given an image basename, or all images if no arguments are provided.''' 789 usage = "usage: %prog [options] <PATH> [PATH ...]" 790 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.''' 791 command_args = shlex.split(line) 792 if not self.image_option_parser: 793 self.image_option_parser = optparse.OptionParser( 794 description=description, prog='image', usage=usage) 795 self.image_option_parser.add_option( 796 '-a', 797 '--all', 798 action='store_true', 799 help='show all images', 800 default=False) 801 try: 802 (options, args) = self.image_option_parser.parse_args(command_args) 803 except: 804 return 805 806 if args: 807 for image_path in args: 808 fullpath_search = image_path[0] == '/' 809 for (crash_log_idx, crash_log) in enumerate(self.crash_logs): 810 matches_found = 0 811 for (image_idx, image) in enumerate(crash_log.images): 812 if fullpath_search: 813 if image.get_resolved_path() == image_path: 814 matches_found += 1 815 print('[%u] ' % (crash_log_idx), image) 816 else: 817 image_basename = image.get_resolved_path_basename() 818 if image_basename == image_path: 819 matches_found += 1 820 print('[%u] ' % (crash_log_idx), image) 821 if matches_found == 0: 822 for (image_idx, image) in enumerate(crash_log.images): 823 resolved_image_path = image.get_resolved_path() 824 if resolved_image_path and string.find( 825 image.get_resolved_path(), image_path) >= 0: 826 print('[%u] ' % (crash_log_idx), image) 827 else: 828 for crash_log in self.crash_logs: 829 for (image_idx, image) in enumerate(crash_log.images): 830 print('[%u] %s' % (image_idx, image)) 831 return False 832 833 834def interactive_crashlogs(debugger, options, args): 835 crash_log_files = list() 836 for arg in args: 837 for resolved_path in glob.glob(arg): 838 crash_log_files.append(resolved_path) 839 840 crash_logs = list() 841 for crash_log_file in crash_log_files: 842 try: 843 crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose) 844 except Exception as e: 845 print(e) 846 continue 847 if options.debug: 848 crash_log.dump() 849 if not crash_log.images: 850 print('error: no images in crash log "%s"' % (crash_log)) 851 continue 852 else: 853 crash_logs.append(crash_log) 854 855 interpreter = Interactive(crash_logs) 856 # List all crash logs that were imported 857 interpreter.do_list() 858 interpreter.cmdloop() 859 860 861def save_crashlog(debugger, command, exe_ctx, result, dict): 862 usage = "usage: %prog [options] <output-path>" 863 description = '''Export the state of current target into a crashlog file''' 864 parser = optparse.OptionParser( 865 description=description, 866 prog='save_crashlog', 867 usage=usage) 868 parser.add_option( 869 '-v', 870 '--verbose', 871 action='store_true', 872 dest='verbose', 873 help='display verbose debug info', 874 default=False) 875 try: 876 (options, args) = parser.parse_args(shlex.split(command)) 877 except: 878 result.PutCString("error: invalid options") 879 return 880 if len(args) != 1: 881 result.PutCString( 882 "error: invalid arguments, a single output file is the only valid argument") 883 return 884 out_file = open(args[0], 'w') 885 if not out_file: 886 result.PutCString( 887 "error: failed to open file '%s' for writing...", 888 args[0]) 889 return 890 target = exe_ctx.target 891 if target: 892 identifier = target.executable.basename 893 process = exe_ctx.process 894 if process: 895 pid = process.id 896 if pid != lldb.LLDB_INVALID_PROCESS_ID: 897 out_file.write( 898 'Process: %s [%u]\n' % 899 (identifier, pid)) 900 out_file.write('Path: %s\n' % (target.executable.fullpath)) 901 out_file.write('Identifier: %s\n' % (identifier)) 902 out_file.write('\nDate/Time: %s\n' % 903 (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) 904 out_file.write( 905 'OS Version: Mac OS X %s (%s)\n' % 906 (platform.mac_ver()[0], subprocess.check_output('sysctl -n kern.osversion', shell=True).decode("utf-8"))) 907 out_file.write('Report Version: 9\n') 908 for thread_idx in range(process.num_threads): 909 thread = process.thread[thread_idx] 910 out_file.write('\nThread %u:\n' % (thread_idx)) 911 for (frame_idx, frame) in enumerate(thread.frames): 912 frame_pc = frame.pc 913 frame_offset = 0 914 if frame.function: 915 block = frame.GetFrameBlock() 916 block_range = block.range[frame.addr] 917 if block_range: 918 block_start_addr = block_range[0] 919 frame_offset = frame_pc - block_start_addr.GetLoadAddress(target) 920 else: 921 frame_offset = frame_pc - frame.function.addr.GetLoadAddress(target) 922 elif frame.symbol: 923 frame_offset = frame_pc - frame.symbol.addr.GetLoadAddress(target) 924 out_file.write( 925 '%-3u %-32s 0x%16.16x %s' % 926 (frame_idx, frame.module.file.basename, frame_pc, frame.name)) 927 if frame_offset > 0: 928 out_file.write(' + %u' % (frame_offset)) 929 line_entry = frame.line_entry 930 if line_entry: 931 if options.verbose: 932 # This will output the fullpath + line + column 933 out_file.write(' %s' % (line_entry)) 934 else: 935 out_file.write( 936 ' %s:%u' % 937 (line_entry.file.basename, line_entry.line)) 938 column = line_entry.column 939 if column: 940 out_file.write(':%u' % (column)) 941 out_file.write('\n') 942 943 out_file.write('\nBinary Images:\n') 944 for module in target.modules: 945 text_segment = module.section['__TEXT'] 946 if text_segment: 947 text_segment_load_addr = text_segment.GetLoadAddress(target) 948 if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS: 949 text_segment_end_load_addr = text_segment_load_addr + text_segment.size 950 identifier = module.file.basename 951 module_version = '???' 952 module_version_array = module.GetVersion() 953 if module_version_array: 954 module_version = '.'.join( 955 map(str, module_version_array)) 956 out_file.write( 957 ' 0x%16.16x - 0x%16.16x %s (%s - ???) <%s> %s\n' % 958 (text_segment_load_addr, 959 text_segment_end_load_addr, 960 identifier, 961 module_version, 962 module.GetUUIDString(), 963 module.file.fullpath)) 964 out_file.close() 965 else: 966 result.PutCString("error: invalid target") 967 968 969def Symbolicate(debugger, command, result, dict): 970 try: 971 SymbolicateCrashLogs(debugger, shlex.split(command)) 972 except Exception as e: 973 result.PutCString("error: python exception: %s" % e) 974 975 976def SymbolicateCrashLog(crash_log, options): 977 if options.debug: 978 crash_log.dump() 979 if not crash_log.images: 980 print('error: no images in crash log') 981 return 982 983 if options.dump_image_list: 984 print("Binary Images:") 985 for image in crash_log.images: 986 if options.verbose: 987 print(image.debug_dump()) 988 else: 989 print(image) 990 991 target = crash_log.create_target() 992 if not target: 993 return 994 exe_module = target.GetModuleAtIndex(0) 995 images_to_load = list() 996 loaded_images = list() 997 if options.load_all_images: 998 # --load-all option was specified, load everything up 999 for image in crash_log.images: 1000 images_to_load.append(image) 1001 else: 1002 # Only load the images found in stack frames for the crashed threads 1003 if options.crashed_only: 1004 for thread in crash_log.threads: 1005 if thread.did_crash(): 1006 for ident in thread.idents: 1007 images = crash_log.find_images_with_identifier(ident) 1008 if images: 1009 for image in images: 1010 images_to_load.append(image) 1011 else: 1012 print('error: can\'t find image for identifier "%s"' % ident) 1013 else: 1014 for ident in crash_log.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 1022 for image in images_to_load: 1023 if image not in loaded_images: 1024 err = image.add_module(target) 1025 if err: 1026 print(err) 1027 else: 1028 loaded_images.append(image) 1029 1030 if crash_log.backtraces: 1031 for thread in crash_log.backtraces: 1032 thread.dump_symbolicated(crash_log, options) 1033 print() 1034 1035 for thread in crash_log.threads: 1036 thread.dump_symbolicated(crash_log, options) 1037 print() 1038 1039 1040def CreateSymbolicateCrashLogOptions( 1041 command_name, 1042 description, 1043 add_interactive_options): 1044 usage = "usage: %prog [options] <FILE> [FILE ...]" 1045 option_parser = optparse.OptionParser( 1046 description=description, prog='crashlog', usage=usage) 1047 option_parser.add_option( 1048 '--verbose', 1049 '-v', 1050 action='store_true', 1051 dest='verbose', 1052 help='display verbose debug info', 1053 default=False) 1054 option_parser.add_option( 1055 '--debug', 1056 '-g', 1057 action='store_true', 1058 dest='debug', 1059 help='display verbose debug logging', 1060 default=False) 1061 option_parser.add_option( 1062 '--load-all', 1063 '-a', 1064 action='store_true', 1065 dest='load_all_images', 1066 help='load all executable images, not just the images found in the crashed stack frames', 1067 default=False) 1068 option_parser.add_option( 1069 '--images', 1070 action='store_true', 1071 dest='dump_image_list', 1072 help='show image list', 1073 default=False) 1074 option_parser.add_option( 1075 '--debug-delay', 1076 type='int', 1077 dest='debug_delay', 1078 metavar='NSEC', 1079 help='pause for NSEC seconds for debugger', 1080 default=0) 1081 option_parser.add_option( 1082 '--crashed-only', 1083 '-c', 1084 action='store_true', 1085 dest='crashed_only', 1086 help='only symbolicate the crashed thread', 1087 default=False) 1088 option_parser.add_option( 1089 '--disasm-depth', 1090 '-d', 1091 type='int', 1092 dest='disassemble_depth', 1093 help='set the depth in stack frames that should be disassembled (default is 1)', 1094 default=1) 1095 option_parser.add_option( 1096 '--disasm-all', 1097 '-D', 1098 action='store_true', 1099 dest='disassemble_all_threads', 1100 help='enabled disassembly of frames on all threads (not just the crashed thread)', 1101 default=False) 1102 option_parser.add_option( 1103 '--disasm-before', 1104 '-B', 1105 type='int', 1106 dest='disassemble_before', 1107 help='the number of instructions to disassemble before the frame PC', 1108 default=4) 1109 option_parser.add_option( 1110 '--disasm-after', 1111 '-A', 1112 type='int', 1113 dest='disassemble_after', 1114 help='the number of instructions to disassemble after the frame PC', 1115 default=4) 1116 option_parser.add_option( 1117 '--source-context', 1118 '-C', 1119 type='int', 1120 metavar='NLINES', 1121 dest='source_context', 1122 help='show NLINES source lines of source context (default = 4)', 1123 default=4) 1124 option_parser.add_option( 1125 '--source-frames', 1126 type='int', 1127 metavar='NFRAMES', 1128 dest='source_frames', 1129 help='show source for NFRAMES (default = 4)', 1130 default=4) 1131 option_parser.add_option( 1132 '--source-all', 1133 action='store_true', 1134 dest='source_all', 1135 help='show source for all threads, not just the crashed thread', 1136 default=False) 1137 if add_interactive_options: 1138 option_parser.add_option( 1139 '-i', 1140 '--interactive', 1141 action='store_true', 1142 help='parse all crash logs and enter interactive mode', 1143 default=False) 1144 return option_parser 1145 1146 1147def SymbolicateCrashLogs(debugger, command_args): 1148 description = '''Symbolicate one or more darwin crash log files to provide source file and line information, 1149inlined stack frames back to the concrete functions, and disassemble the location of the crash 1150for the first frame of the crashed thread. 1151If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter 1152for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been 1153created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows 1154you to explore the program as if it were stopped at the locations described in the crash log and functions can 1155be disassembled and lookups can be performed using the addresses found in the crash log.''' 1156 option_parser = CreateSymbolicateCrashLogOptions( 1157 'crashlog', description, True) 1158 try: 1159 (options, args) = option_parser.parse_args(command_args) 1160 except: 1161 return 1162 1163 if options.debug: 1164 print('command_args = %s' % command_args) 1165 print('options', options) 1166 print('args', args) 1167 1168 if options.debug_delay > 0: 1169 print("Waiting %u seconds for debugger to attach..." % options.debug_delay) 1170 time.sleep(options.debug_delay) 1171 error = lldb.SBError() 1172 1173 if args: 1174 if options.interactive: 1175 interactive_crashlogs(debugger, options, args) 1176 else: 1177 for crash_log_file in args: 1178 crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose) 1179 SymbolicateCrashLog(crash_log, options) 1180if __name__ == '__main__': 1181 # Create a new debugger instance 1182 debugger = lldb.SBDebugger.Create() 1183 SymbolicateCrashLogs(debugger, sys.argv[1:]) 1184 lldb.SBDebugger.Destroy(debugger) 1185elif getattr(lldb, 'debugger', None): 1186 lldb.debugger.HandleCommand( 1187 'command script add -f lldb.macosx.crashlog.Symbolicate crashlog') 1188 lldb.debugger.HandleCommand( 1189 'command script add -f lldb.macosx.crashlog.save_crashlog save_crashlog') 1190