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