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