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