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