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