3 #----------------------------------------------------------------------
4 # Be sure to add the python path that points to the LLDB shared library.
6 # To use this in the embedded python interpreter using "lldb":
8 # cd /path/containing/crashlog.py
10 # (lldb) script import crashlog
11 # "crashlog" command installed, type "crashlog --help" for detailed help
12 # (lldb) crashlog ~/Library/Logs/DiagnosticReports/a.crash
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.
22 # On MacOSX csh, tcsh:
23 # ( setenv PYTHONPATH /path/to/LLDB.framework/Resources/Python ; ./crashlog.py ~/Library/Logs/DiagnosticReports/a.crash )
26 # PYTHONPATH=/path/to/LLDB.framework/Resources/Python ./crashlog.py ~/Library/Logs/DiagnosticReports/a.crash
27 #----------------------------------------------------------------------
29 from __future__
import print_function
47 # First try for LLDB in case PYTHONPATH is already correctly setup.
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.
63 print("error: couldn't locate the 'lldb' module, please set PYTHONPATH correctly")
66 from lldb
.utils
import symbolication
70 if sys
.version_info
.major
== 3:
71 return plistlib
.loads(s
)
73 return plistlib
.readPlistFromString(s
)
75 class CrashLog(symbolication
.Symbolicator
):
77 """Class that represents a thread in a darwin crash log"""
79 def __init__(self
, index
, app_specific_backtrace
):
83 self
.registers
= dict()
86 self
.app_specific_backtrace
= app_specific_backtrace
88 def dump(self
, prefix
):
89 if self
.app_specific_backtrace
:
90 print("%Application Specific Backtrace[%u] %s" % (prefix
, self
.index
, self
.reason
))
92 print("%sThread[%u] %s" % (prefix
, self
.index
, self
.reason
))
94 print("%s Frames:" % (prefix
))
95 for frame
in self
.frames
:
96 frame
.dump(prefix
+ ' ')
98 print("%s Registers:" % (prefix
))
99 for reg
in self
.registers
.keys():
100 print("%s %-8s = %#16.16x" % (prefix
, reg
, self
.registers
[reg
]))
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:
110 display_frame_idx
= -1
111 for frame_idx
, frame
in enumerate(self
.frames
):
113 this_thread_crashed
or options
.disassemble_all_threads
) and frame_idx
< options
.disassemble_depth
115 symbolicated_frame_addresses
= crash_log
.symbolicate(
116 frame
.pc
& crash_log
.addr_mask
, options
.verbose
)
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
)
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()
135 crash_log
.debugger
.GetSourceManager().DisplaySourceLinesWithLineNumbers(
136 line_entry
.file, line_entry
.line
, source_context
, source_context
, "->", strm
)
137 source_text
= strm
.GetData()
139 # Indent the source a bit
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:
145 instructions
= symbolicated_frame_address
.get_instructions()
148 symbolication
.disassemble_instructions(
149 crash_log
.get_target(),
152 options
.disassemble_before
,
153 options
.disassemble_after
,
156 symbolicated_frame_address_idx
+= 1
161 for reg
in self
.registers
.keys():
162 print(" %-8s = %#16.16x" % (reg
, self
.registers
[reg
]))
164 def add_ident(self
, ident
):
165 if ident
not in self
.idents
:
166 self
.idents
.append(ident
)
169 return self
.reason
is not None
172 if self
.app_specific_backtrace
:
173 s
= "Application Specific Backtrace[%u]" % self
.index
175 s
= "Thread[%u]" % self
.index
177 s
+= ' %s' % self
.reason
181 """Class that represents a stack frame in a thread in a darwin crash log"""
183 def __init__(self
, index
, pc
, description
):
185 self
.description
= description
190 return "[%3u] 0x%16.16x %s" % (
191 self
.index
, self
.pc
, self
.description
)
193 return "[%3u] 0x%16.16x" % (self
.index
, self
.pc
)
195 def dump(self
, prefix
):
196 print("%s%s" % (prefix
, str(self
)))
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
):
203 dsymForUUIDBinary
= subprocess
.check_output('which dsymForUUID',
204 shell
=True).decode("utf-8").rstrip('\n')
206 dsymForUUIDBinary
= ""
208 dwarfdump_uuid_regex
= re
.compile(
209 'UUID: ([-0-9a-fA-F]+) \(([^\(]+)\) .*')
220 symbolication
.Image
.__init
__(self
, path
, uuid
)
222 symbolication
.Section(
226 self
.identifier
= identifier
227 self
.version
= version
228 self
.verbose
= verbose
230 def show_symbol_progress(self
):
232 Hide progress output and errors from system frameworks as they are plentiful.
236 return not (self
.path
.startswith("/System/Library/") or
237 self
.path
.startswith("/usr/lib/"))
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
)
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)
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())))
260 def locate_module_and_debug_symbols(self
):
261 # Don't load a module twice...
264 # Mark this as resolved so we don't keep trying
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)
275 plist_root
= read_plist(s
)
277 print(("Got exception: ", sys
.exc_info()[1], " handling dsymForUUID output: \n", s
))
280 plist
= plist_root
[uuid_str
]
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():
294 if not self
.resolved_path
and not os
.path
.exists(self
.path
):
296 dsym
= subprocess
.check_output(
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
))
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():
310 if (self
.resolved_path
and os
.path
.exists(self
.resolved_path
)) or (
311 self
.path
and os
.path
.exists(self
.path
)):
315 self
.unavailable
= True
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
330 self
.verbose
= verbose
333 print("Crash Log File: %s" % (self
.path
))
335 print("\nApplication Specific Backtraces:")
336 for thread
in self
.backtraces
:
339 for thread
in self
.threads
:
342 for image
in self
.images
:
345 def find_image_with_identifier(self
, identifier
):
346 for image
in self
.images
:
347 if image
.identifier
== identifier
:
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
):
356 def create_target(self
):
357 if self
.target
is None:
358 self
.target
= symbolication
.Symbolicator
.create_target(self
)
361 # We weren't able to open the main executable as, but we can still
363 print('crashlog.create_target()...2')
365 for ident
in self
.idents
:
366 image
= self
.find_image_with_identifier(ident
)
368 self
.target
= image
.create_target(self
.debugger
)
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
)
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.')
382 def get_target(self
):
386 class CrashLogFormatException(Exception):
390 class CrashLogParseException(Exception):
394 class CrashLogParser
:
395 def parse(self
, debugger
, path
, verbose
):
397 return JSONCrashLogParser(debugger
, path
, verbose
).parse()
398 except CrashLogFormatException
:
399 return TextCrashLogParser(debugger
, path
, verbose
).parse()
402 class 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
)
409 with
open(self
.path
, 'r') as f
:
412 # First line is meta-data.
413 buffer = buffer[buffer.index('\n') + 1:]
416 self
.data
= json
.loads(buffer)
418 raise CrashLogFormatException()
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'])
427 thread
.reason
= '{} {}'.format(thread
.reason
, reason
)
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
))
437 def get_used_image(self
, idx
):
438 return self
.data
['usedImages'][idx
]
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']
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'])
454 return "{} ({}){}".format(exception_type
, exception_signal
,
457 def parse_images(self
, json_images
):
459 for json_image
in json_images
:
460 img_uuid
= uuid
.UUID(json_image
['uuid'])
461 low
= int(json_image
['base'])
463 name
= json_image
['name'] if 'name' in json_image
else ''
464 path
= json_image
['path'] if 'path' in json_image
else ''
466 darwin_image
= self
.crashlog
.DarwinImage(low
, high
, name
, version
,
469 self
.crashlog
.images
.append(darwin_image
)
472 def parse_frames(self
, thread
, json_frames
):
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
)
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
))
487 def parse_threads(self
, json_threads
):
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
)
502 def parse_thread_registers(self
, json_thread_state
):
504 for key
, state
in json_thread_state
.items():
506 value
= int(state
['value'])
507 registers
[key
] = value
508 except (TypeError, ValueError):
513 class CrashLogParseMode
:
522 class 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
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
546 def __init__(self
, debugger
, path
, verbose
):
547 self
.path
= os
.path
.expanduser(path
)
548 self
.verbose
= verbose
550 self
.app_specific_backtrace
= False
551 self
.crashlog
= CrashLog(debugger
, self
.path
, self
.verbose
)
552 self
.parse_mode
= CrashLogParseMode
.NORMAL
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
,
563 with
open(self
.path
,'r') as f
:
564 lines
= f
.read().splitlines()
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
)
580 self
.crashlog
.threads
.append(self
.thread
)
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
589 self
.parsers
[self
.parse_mode
](line
)
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
)
607 self
.crashlog
.process_version
= matched_pair
.group(1)
608 self
.crashlog
.process_compatability_version
= matched_pair
.group(
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(
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()
621 elif line
.startswith('Exception Codes:'):
622 self
.crashlog
.thread_exception_data
= line
[16:].strip()
624 elif line
.startswith('Exception Subtype:'): # iOS
625 self
.crashlog
.thread_exception_data
= line
[18:].strip()
627 elif line
.startswith('Crashed Thread:'):
628 self
.crashlog
.crashed_thread_idx
= int(line
[15:].strip().split()[0])
630 elif line
.startswith('Triggered by Thread:'): # iOS
631 self
.crashlog
.crashed_thread_idx
= int(line
[20:].strip().split()[0])
633 elif line
.startswith('Report Version:'):
634 self
.crashlog
.version
= int(line
[15:].strip())
636 elif line
.startswith('System Profile:'):
637 self
.parse_mode
= CrashLogParseMode
.SYSTEM
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:')):
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
]
656 thread_insts_match
= self
.thread_instrs_regex
.search(line
)
657 if thread_insts_match
:
658 self
.parse_mode
= CrashLogParseMode
.INSTRS
660 thread_match
= self
.thread_regex
.search(line
)
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)
668 elif line
.startswith('Binary Images:'):
669 self
.parse_mode
= CrashLogParseMode
.IMAGES
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
682 self
.thread
= self
.crashlog
.Thread(idx
, True)
683 self
.crashlog
.info_lines
.append(line
.strip())
685 def parse_thread(self
, line
):
686 if line
.startswith('Thread'):
688 if self
.null_frame_regex
.search(line
):
689 print('warning: thread parser ignored null-frame: "%s"' % line
)
691 frame_match
= self
.frame_regex
.search(line
)
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
))
702 print('error: frame regex failed for line: "%s"' % line
)
704 def parse_images(self
, line
):
705 image_match
= self
.image_regex_uuid
.search(line
)
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),
712 if img_version
else "",
713 uuid
.UUID(img_uuid
), img_path
,
715 self
.crashlog
.images
.append(image
)
717 print("error: image regex failed for: %s" % line
)
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)
729 def parse_system(self
, line
):
730 self
.crashlog
.system_profile
.append(line
)
732 def parse_instructions(self
, line
):
737 print("Usage: lldb-symbolicate.py [-n name] executable-image")
741 class 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
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
752 def default(self
, line
):
753 '''Catch all for unknown command, which will exit the interpreter.'''
754 print("uknown command: %s" % line
)
757 def do_q(self
, line
):
761 def do_quit(self
, line
):
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
)
773 (options
, args
) = option_parser
.parse_args(command_args
)
778 # We have arguments, they must valid be crash log file indexes
781 if idx
< len(self
.crash_logs
):
782 SymbolicateCrashLog(self
.crash_logs
[idx
], options
)
784 print('error: crash log index %u is out of range' % (idx
))
786 # No arguments, symbolicate all crash logs using the options
788 for idx
in range(len(self
.crash_logs
)):
789 SymbolicateCrashLog(self
.crash_logs
[idx
], options
)
791 def do_list(self
, line
=None):
792 '''Dump a list of all crash logs that are currently loaded.
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
))
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(
811 help='show all images',
814 (options
, args
) = self
.image_option_parser
.parse_args(command_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
):
823 for (image_idx
, image
) in enumerate(crash_log
.images
):
825 if image
.get_resolved_path() == image_path
:
827 print('[%u] ' % (crash_log_idx
), image
)
829 image_basename
= image
.get_resolved_path_basename()
830 if image_basename
== image_path
:
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
)
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
))
846 def interactive_crashlogs(debugger
, options
, args
):
847 crash_log_files
= list()
849 for resolved_path
in glob
.glob(arg
):
850 crash_log_files
.append(resolved_path
)
853 for crash_log_file
in crash_log_files
:
855 crash_log
= CrashLogParser().parse(debugger
, crash_log_file
, options
.verbose
)
856 except Exception as e
:
861 if not crash_log
.images
:
862 print('error: no images in crash log "%s"' % (crash_log
))
865 crash_logs
.append(crash_log
)
867 interpreter
= Interactive(crash_logs
)
868 # List all crash logs that were imported
869 interpreter
.do_list()
870 interpreter
.cmdloop()
873 def 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',
885 help='display verbose debug info',
888 (options
, args
) = parser
.parse_args(shlex
.split(command
))
890 result
.PutCString("error: invalid options")
894 "error: invalid arguments, a single output file is the only valid argument")
896 out_file
= open(args
[0], 'w')
899 "error: failed to open file '%s' for writing...",
902 target
= exe_ctx
.target
904 identifier
= target
.executable
.basename
905 process
= exe_ctx
.process
908 if pid
!= lldb
.LLDB_INVALID_PROCESS_ID
:
910 'Process: %s [%u]\n' %
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")))
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
):
927 block
= frame
.GetFrameBlock()
928 block_range
= block
.range[frame
.addr
]
930 block_start_addr
= block_range
[0]
931 frame_offset
= frame_pc
- block_start_addr
.GetLoadAddress(target
)
933 frame_offset
= frame_pc
- frame
.function
.addr
.GetLoadAddress(target
)
935 frame_offset
= frame_pc
- frame
.symbol
.addr
.GetLoadAddress(target
)
937 '%-3u %-32s 0x%16.16x %s' %
938 (frame_idx
, frame
.module
.file.basename
, frame_pc
, frame
.name
))
940 out_file
.write(' + %u' % (frame_offset
))
941 line_entry
= frame
.line_entry
944 # This will output the fullpath + line + column
945 out_file
.write(' %s' % (line_entry
))
949 (line_entry
.file.basename
, line_entry
.line
))
950 column
= line_entry
.column
952 out_file
.write(':%u' % (column
))
955 out_file
.write('\nBinary Images:\n')
956 for module
in target
.modules
:
957 text_segment
= module
.section
['__TEXT']
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
))
969 ' 0x%16.16x - 0x%16.16x %s (%s - ???) <%s> %s\n' %
970 (text_segment_load_addr
,
971 text_segment_end_load_addr
,
974 module
.GetUUIDString(),
975 module
.file.fullpath
))
978 result
.PutCString("error: invalid target")
981 def Symbolicate(debugger
, command
, result
, dict):
983 SymbolicateCrashLogs(debugger
, shlex
.split(command
))
984 except Exception as e
:
985 result
.PutCString("error: python exception: %s" % e
)
988 def SymbolicateCrashLog(crash_log
, options
):
991 if not crash_log
.images
:
992 print('error: no images in crash log')
995 if options
.dump_image_list
:
996 print("Binary Images:")
997 for image
in crash_log
.images
:
999 print(image
.debug_dump())
1003 target
= crash_log
.create_target()
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
)
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
)
1021 for image
in images
:
1022 images_to_load
.append(image
)
1024 print('error: can\'t find image for identifier "%s"' % ident
)
1026 for ident
in crash_log
.idents
:
1027 images
= crash_log
.find_images_with_identifier(ident
)
1029 for image
in images
:
1030 images_to_load
.append(image
)
1032 print('error: can\'t find image for identifier "%s"' % ident
)
1034 for image
in images_to_load
:
1035 if image
not in loaded_images
:
1036 err
= image
.add_module(target
)
1040 loaded_images
.append(image
)
1042 if crash_log
.backtraces
:
1043 for thread
in crash_log
.backtraces
:
1044 thread
.dump_symbolicated(crash_log
, options
)
1047 for thread
in crash_log
.threads
:
1048 thread
.dump_symbolicated(crash_log
, options
)
1052 def CreateSymbolicateCrashLogOptions(
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(
1062 action
='store_true',
1064 help='display verbose debug info',
1066 option_parser
.add_option(
1069 action
='store_true',
1071 help='display verbose debug logging',
1073 option_parser
.add_option(
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',
1080 option_parser
.add_option(
1082 action
='store_true',
1083 dest
='dump_image_list',
1084 help='show image list',
1086 option_parser
.add_option(
1091 help='pause for NSEC seconds for debugger',
1093 option_parser
.add_option(
1096 action
='store_true',
1097 dest
='crashed_only',
1098 help='only symbolicate the crashed thread',
1100 option_parser
.add_option(
1104 dest
='disassemble_depth',
1105 help='set the depth in stack frames that should be disassembled (default is 1)',
1107 option_parser
.add_option(
1110 action
='store_true',
1111 dest
='disassemble_all_threads',
1112 help='enabled disassembly of frames on all threads (not just the crashed thread)',
1114 option_parser
.add_option(
1118 dest
='disassemble_before',
1119 help='the number of instructions to disassemble before the frame PC',
1121 option_parser
.add_option(
1125 dest
='disassemble_after',
1126 help='the number of instructions to disassemble after the frame PC',
1128 option_parser
.add_option(
1133 dest
='source_context',
1134 help='show NLINES source lines of source context (default = 4)',
1136 option_parser
.add_option(
1140 dest
='source_frames',
1141 help='show source for NFRAMES (default = 4)',
1143 option_parser
.add_option(
1145 action
='store_true',
1147 help='show source for all threads, not just the crashed thread',
1149 if add_interactive_options
:
1150 option_parser
.add_option(
1153 action
='store_true',
1154 help='parse all crash logs and enter interactive mode',
1156 return option_parser
1159 def SymbolicateCrashLogs(debugger
, command_args
):
1160 description
= '''Symbolicate one or more darwin crash log files to provide source file and line information,
1161 inlined stack frames back to the concrete functions, and disassemble the location of the crash
1162 for the first frame of the crashed thread.
1163 If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter
1164 for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been
1165 created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows
1166 you to explore the program as if it were stopped at the locations described in the crash log and functions can
1167 be disassembled and lookups can be performed using the addresses found in the crash log.'''
1168 option_parser
= CreateSymbolicateCrashLogOptions(
1169 'crashlog', description
, True)
1171 (options
, args
) = option_parser
.parse_args(command_args
)
1176 print('command_args = %s' % command_args
)
1177 print('options', options
)
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()
1186 if options
.interactive
:
1187 interactive_crashlogs(debugger
, options
, args
)
1189 for crash_log_file
in args
:
1190 crash_log
= CrashLogParser().parse(debugger
, crash_log_file
, options
.verbose
)
1191 SymbolicateCrashLog(crash_log
, options
)
1192 if __name__
== '__main__':
1193 # Create a new debugger instance
1194 debugger
= lldb
.SBDebugger
.Create()
1195 SymbolicateCrashLogs(debugger
, sys
.argv
[1:])
1196 lldb
.SBDebugger
.Destroy(debugger
)
1197 elif 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')