[lldb/crashlog] Add CrashLogScriptedProcess & remove interactive mode

This patch introduces a new type of ScriptedProcess: CrashLogScriptedProcess.
It takes advantage of lldb's crashlog parsers and Scripted Processes to
reconstruct a static debugging session with symbolicated stackframes, instead
of just dumping out everything in the user's terminal.

The crashlog command also has an interactive mode that only provide a
very limited experience. This is why this patch removes all the logic
for this interactive mode and creates CrashLogScriptedProcess instead.

This will fetch and load all the libraries that were used by the crashed
thread and re-create all the frames artificially.

rdar://88721117

Differential Revision: https://reviews.llvm.org/D119501

Signed-off-by: Med Ismail Bennani <medismail.bennani@gmail.com>
This commit is contained in:
Med Ismail Bennani 2022-02-16 11:43:44 -08:00
parent 7f3fc2eee8
commit 7c54ffdc6c
6 changed files with 296 additions and 137 deletions

View file

@ -114,6 +114,7 @@ function(finish_swig_python swig_target lldb_python_bindings_dir lldb_python_tar
${swig_target}
${lldb_python_target_dir} "macosx"
FILES "${LLDB_SOURCE_DIR}/examples/python/crashlog.py"
"${LLDB_SOURCE_DIR}/examples/python/scripted_process/crashlog_scripted_process.py"
"${LLDB_SOURCE_DIR}/examples/darwin/heap_find/heap.py")
create_python_package(

View file

@ -63,7 +63,6 @@ except ImportError:
from lldb.utils import symbolication
def read_plist(s):
if sys.version_info.major == 3:
return plistlib.loads(s)
@ -780,138 +779,6 @@ def usage():
sys.exit(0)
class Interactive(cmd.Cmd):
'''Interactive prompt for analyzing one or more Darwin crash logs, type "help" to see a list of supported commands.'''
image_option_parser = None
def __init__(self, crash_logs):
cmd.Cmd.__init__(self)
self.use_rawinput = False
self.intro = 'Interactive crashlogs prompt, type "help" to see a list of supported commands.'
self.crash_logs = crash_logs
self.prompt = '% '
def default(self, line):
'''Catch all for unknown command, which will exit the interpreter.'''
print("uknown command: %s" % line)
return True
def do_q(self, line):
'''Quit command'''
return True
def do_quit(self, line):
'''Quit command'''
return True
def do_symbolicate(self, line):
description = '''Symbolicate one or more darwin crash log files by index to provide source file and line information,
inlined stack frames back to the concrete functions, and disassemble the location of the crash
for the first frame of the crashed thread.'''
option_parser = CreateSymbolicateCrashLogOptions(
'symbolicate', description, False)
command_args = shlex.split(line)
try:
(options, args) = option_parser.parse_args(command_args)
except:
return
if args:
# We have arguments, they must valid be crash log file indexes
for idx_str in args:
idx = int(idx_str)
if idx < len(self.crash_logs):
SymbolicateCrashLog(self.crash_logs[idx], options)
else:
print('error: crash log index %u is out of range' % (idx))
else:
# No arguments, symbolicate all crash logs using the options
# provided
for idx in range(len(self.crash_logs)):
SymbolicateCrashLog(self.crash_logs[idx], options)
def do_list(self, line=None):
'''Dump a list of all crash logs that are currently loaded.
USAGE: list'''
print('%u crash logs are loaded:' % len(self.crash_logs))
for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
print('[%u] = %s' % (crash_log_idx, crash_log.path))
def do_image(self, line):
'''Dump information about one or more binary images in the crash log given an image basename, or all images if no arguments are provided.'''
usage = "usage: %prog [options] <PATH> [PATH ...]"
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.'''
command_args = shlex.split(line)
if not self.image_option_parser:
self.image_option_parser = optparse.OptionParser(
description=description, prog='image', usage=usage)
self.image_option_parser.add_option(
'-a',
'--all',
action='store_true',
help='show all images',
default=False)
try:
(options, args) = self.image_option_parser.parse_args(command_args)
except:
return
if args:
for image_path in args:
fullpath_search = image_path[0] == '/'
for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
matches_found = 0
for (image_idx, image) in enumerate(crash_log.images):
if fullpath_search:
if image.get_resolved_path() == image_path:
matches_found += 1
print('[%u] ' % (crash_log_idx), image)
else:
image_basename = image.get_resolved_path_basename()
if image_basename == image_path:
matches_found += 1
print('[%u] ' % (crash_log_idx), image)
if matches_found == 0:
for (image_idx, image) in enumerate(crash_log.images):
resolved_image_path = image.get_resolved_path()
if resolved_image_path and string.find(
image.get_resolved_path(), image_path) >= 0:
print('[%u] ' % (crash_log_idx), image)
else:
for crash_log in self.crash_logs:
for (image_idx, image) in enumerate(crash_log.images):
print('[%u] %s' % (image_idx, image))
return False
def interactive_crashlogs(debugger, options, args):
crash_log_files = list()
for arg in args:
for resolved_path in glob.glob(arg):
crash_log_files.append(resolved_path)
crash_logs = list()
for crash_log_file in crash_log_files:
try:
crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose)
except Exception as e:
print(e)
continue
if options.debug:
crash_log.dump()
if not crash_log.images:
print('error: no images in crash log "%s"' % (crash_log))
continue
else:
crash_logs.append(crash_log)
interpreter = Interactive(crash_logs)
# List all crash logs that were imported
interpreter.do_list()
interpreter.cmdloop()
def save_crashlog(debugger, command, exe_ctx, result, dict):
usage = "usage: %prog [options] <output-path>"
description = '''Export the state of current target into a crashlog file'''
@ -1106,6 +973,43 @@ def SymbolicateCrashLog(crash_log, options):
for error in crash_log.errors:
print(error)
def load_crashlog_in_scripted_process(debugger, crash_log_file):
result = lldb.SBCommandReturnObject()
crashlog_path = os.path.expanduser(crash_log_file)
if not os.path.exists(crashlog_path):
result.PutCString("error: crashlog file %s does not exist" % crashlog_path)
try:
crashlog = CrashLogParser().parse(debugger, crashlog_path, False)
except Exception as e:
result.PutCString("error: python exception: %s" % e)
return
target = crashlog.create_target()
if not target:
result.PutCString("error: couldn't create target")
return
ci = debugger.GetCommandInterpreter()
if not ci:
result.PutCString("error: couldn't get command interpreter")
return
res = lldb.SBCommandReturnObject()
ci.HandleCommand('script from lldb.macosx import crashlog_scripted_process', res)
if not res.Succeeded():
result.PutCString("error: couldn't import crashlog scripted process module")
return
structured_data = lldb.SBStructuredData()
structured_data.SetFromJSON(json.dumps({ "crashlog_path" : crashlog_path }))
launch_info = lldb.SBLaunchInfo(None)
launch_info.SetProcessPluginName("ScriptedProcess")
launch_info.SetScriptedProcessClassName("crashlog_scripted_process.CrashLogScriptedProcess")
launch_info.SetScriptedProcessDictionary(structured_data)
error = lldb.SBError()
process = target.Launch(launch_info, error)
def CreateSymbolicateCrashLogOptions(
command_name,
@ -1209,8 +1113,14 @@ def CreateSymbolicateCrashLogOptions(
'-i',
'--interactive',
action='store_true',
help='parse all crash logs and enter interactive mode',
help='parse a crash log and load it in a ScriptedProcess',
default=False)
option_parser.add_option(
'-b',
'--batch',
action='store_true',
help='dump symbolicated stackframes without creating a debug session',
default=True)
return option_parser
@ -1242,11 +1152,23 @@ def SymbolicateCrashLogs(debugger, command_args):
time.sleep(options.debug_delay)
error = lldb.SBError()
if args:
def should_run_in_interactive_mode(options, ci):
if options.interactive:
interactive_crashlogs(debugger, options, args)
return True
elif options.batch:
return False
# elif ci and ci.IsInteractive():
# return True
else:
for crash_log_file in args:
return False
ci = debugger.GetCommandInterpreter()
if args:
for crash_log_file in args:
if should_run_in_interactive_mode(options, ci):
load_crashlog_in_scripted_process(debugger, crash_log_file)
else:
crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose)
SymbolicateCrashLog(crash_log, options)

View file

@ -0,0 +1,148 @@
import os,json,struct,signal
from typing import Any, Dict
import lldb
from lldb.plugins.scripted_process import ScriptedProcess
from lldb.plugins.scripted_process import ScriptedThread
from lldb.macosx.crashlog import CrashLog,CrashLogParser
class CrashLogScriptedProcess(ScriptedProcess):
def parse_crashlog(self):
try:
crash_log = CrashLogParser().parse(self.dbg, self.crashlog_path, False)
except Exception as e:
return
self.pid = crash_log.process_id
self.crashed_thread_idx = crash_log.crashed_thread_idx
self.loaded_images = []
for thread in crash_log.threads:
if thread.did_crash():
for ident in thread.idents:
images = crash_log.find_images_with_identifier(ident)
if images:
for image in images:
#TODO: Add to self.loaded_images and load images in lldb
err = image.add_module(self.target)
if err:
print(err)
else:
self.loaded_images.append(image)
self.threads[thread.index] = CrashLogScriptedThread(self, None, thread)
def __init__(self, target: lldb.SBTarget, args : lldb.SBStructuredData):
super().__init__(target, args)
if not self.target or not self.target.IsValid():
return
self.crashlog_path = None
crashlog_path = args.GetValueForKey("crashlog_path")
if crashlog_path and crashlog_path.IsValid():
if crashlog_path.GetType() == lldb.eStructuredDataTypeString:
self.crashlog_path = crashlog_path.GetStringValue(4096)
if not self.crashlog_path:
return
self.pid = super().get_process_id()
self.crashed_thread_idx = 0
self.parse_crashlog()
def get_memory_region_containing_address(self, addr: int) -> lldb.SBMemoryRegionInfo:
return None
def get_thread_with_id(self, tid: int):
return {}
def get_registers_for_thread(self, tid: int):
return {}
def read_memory_at_address(self, addr: int, size: int) -> lldb.SBData:
# NOTE: CrashLogs don't contain any memory.
return lldb.SBData()
def get_loaded_images(self):
# TODO: Iterate over corefile_target modules and build a data structure
# from it.
return self.loaded_images
def get_process_id(self) -> int:
return self.pid
def should_stop(self) -> bool:
return True
def is_alive(self) -> bool:
return True
def get_scripted_thread_plugin(self):
return CrashLogScriptedThread.__module__ + "." + CrashLogScriptedThread.__name__
class CrashLogScriptedThread(ScriptedThread):
def create_register_ctx(self):
if not self.has_crashed:
return dict.fromkeys([*map(lambda reg: reg['name'], self.register_info['registers'])] , 0)
if not self.backing_thread or not len(self.backing_thread.registers):
return dict.fromkeys([*map(lambda reg: reg['name'], self.register_info['registers'])] , 0)
for reg in self.register_info['registers']:
reg_name = reg['name']
if reg_name in self.backing_thread.registers:
self.register_ctx[reg_name] = self.backing_thread.registers[reg_name]
else:
self.register_ctx[reg_name] = 0
return self.register_ctx
def create_stackframes(self):
if not self.has_crashed:
return None
if not self.backing_thread or not len(self.backing_thread.frames):
return None
for frame in self.backing_thread.frames:
sym_addr = lldb.SBAddress()
sym_addr.SetLoadAddress(frame.pc, self.target)
if not sym_addr.IsValid():
continue
self.frames.append({"idx": frame.index, "pc": frame.pc})
return self.frames
def __init__(self, process, args, crashlog_thread):
super().__init__(process, args)
self.backing_thread = crashlog_thread
self.idx = self.backing_thread.index
self.has_crashed = (self.scripted_process.crashed_thread_idx == self.idx)
self.create_stackframes()
def get_thread_id(self) -> int:
return self.idx
def get_name(self) -> str:
return CrashLogScriptedThread.__name__ + ".thread-" + str(self.idx)
def get_state(self):
if not self.has_crashed:
return lldb.eStateStopped
return lldb.eStateCrashed
def get_stop_reason(self) -> Dict[str, Any]:
if not self.has_crashed:
return { "type": lldb.eStopReasonNone, "data": { }}
# TODO: Investigate what stop reason should be reported when crashed
return { "type": lldb.eStopReasonException, "data": { "desc": "EXC_BAD_ACCESS" }}
def get_register_context(self) -> str:
if not self.register_ctx:
self.register_ctx = self.create_register_ctx()
return struct.pack("{}Q".format(len(self.register_ctx)), *self.register_ctx.values())

View file

@ -303,6 +303,9 @@ bool ScriptedProcess::DoUpdateThreadList(ThreadList &old_thread_list,
StructuredData::DictionarySP thread_info_sp = GetInterface().GetThreadsInfo();
// FIXME: Need to sort the dictionary otherwise the thread ids won't match the
// thread indices.
if (!thread_info_sp)
return ScriptedInterface::ErrorWithMessage<bool>(
LLVM_PRETTY_FUNCTION,

View file

@ -0,0 +1,75 @@
{"app_name":"scripted_crashlog_json.test.tmp.out","timestamp":"2022-02-14 16:30:31.00 -0800","app_version":"","slice_uuid":"b928ee77-9429-334f-ac88-41440bb3d4c7","build_version":"","platform":1,"share_with_app_devs":0,"is_first_party":1,"bug_type":"309","os_version":"macOS 12.3","incident_id":"E57CADE7-DC44-45CE-8D16-18EBC4406B97","name":"scripted_crashlog_json.test.tmp.out"}
{
"uptime" : 260000,
"procLaunch" : "2022-02-14 16:30:31.8048 -0800",
"procRole" : "Unspecified",
"version" : 2,
"userID" : 501,
"deployVersion" : 210,
"modelCode" : "MacBookPro18,2",
"procStartAbsTime" : 6478056069413,
"coalitionID" : 22196,
"osVersion" : {
"train" : "macOS 12.3",
"build" : "",
"releaseType" : ""
},
"captureTime" : "2022-02-14 16:30:31.8096 -0800",
"incident" : "E57CADE7-DC44-45CE-8D16-18EBC4406B97",
"bug_type" : "309",
"pid" : 92190,
"procExitAbsTime" : 6478056175721,
"translated" : false,
"cpuType" : "ARM-64",
"procName" : "scripted_crashlog_json.test.tmp.out",
"procPath" : "\/Users\/USER\/*\/scripted_crashlog_json.test.tmp.out",
"parentProc" : "zsh",
"parentPid" : 82132,
"coalitionName" : "com.apple.Terminal",
"crashReporterKey" : "CDC11418-EDBF-2A49-0D83-8B441A5004B0",
"responsiblePid" : 76395,
"responsibleProc" : "Terminal",
"wakeTime" : 14889,
"sleepWakeUUID" : "BCA947AE-2F0A-44C7-8445-FEDFFA236CD0",
"sip" : "enabled",
"vmRegionInfo" : "0 is not in any region. Bytes before following region: 4372692992\n REGION TYPE START - END [ VSIZE] PRT\/MAX SHRMOD REGION DETAIL\n UNUSED SPACE AT START\n---> \n __TEXT 104a20000-104a24000 [ 16K] r-x\/r-x SM=COW ....test.tmp.out",
"isCorpse" : 1,
"exception" : {"codes":"0x0000000000000001, 0x0000000000000000","rawCodes":[1,0],"type":"EXC_BAD_ACCESS","signal":"SIGSEGV","subtype":"KERN_INVALID_ADDRESS at 0x0000000000000000"},
"termination" : {"flags":0,"code":11,"namespace":"SIGNAL","indicator":"Segmentation fault: 11","byProc":"exc handler","byPid":92190},
"vmregioninfo" : "0 is not in any region. Bytes before following region: 4372692992\n REGION TYPE START - END [ VSIZE] PRT\/MAX SHRMOD REGION DETAIL\n UNUSED SPACE AT START\n---> \n __TEXT 104a20000-104a24000 [ 16K] r-x\/r-x SM=COW ....test.tmp.out",
"extMods" : {"caller":{"thread_create":0,"thread_set_state":0,"task_for_pid":0},"system":{"thread_create":0,"thread_set_state":156,"task_for_pid":28},"targeted":{"thread_create":0,"thread_set_state":0,"task_for_pid":0},"warnings":0},
"faultingThread" : 0,
"threads" : [{"triggered":true,"id":4567339,"threadState":{"x":[{"value":1},{"value":6094187136},{"value":6094187152},{"value":6094187720},{"value":0},{"value":0},{"value":0},{"value":0},{"value":1},{"value":0},{"value":0},{"value":2},{"value":2},{"value":0},{"value":80},{"value":0},{"value":13118353544},{"value":7701436843874442528},{"value":0},{"value":4373676128},{"sourceLine":8,"value":4372709256,"sourceFile":"test.c","symbol":"main","symbolLocation":0},{"value":4373332080,"symbolLocation":0,"symbol":"dyld4::sConfigBuffer"},{"value":0},{"value":0},{"value":0},{"value":0},{"value":0},{"value":0},{"value":0}],"flavor":"ARM_THREAD_STATE64","lr":{"value":4372709248},"cpsr":{"value":1610616832},"fp":{"value":6094186736},"sp":{"value":6094186720},"esr":{"value":2449473606,"description":"(Data Abort) byte write Translation fault"},"pc":{"value":4372709224,"matchesCrashFrame":1},"far":{"value":0}},"queue":"com.apple.main-thread","frames":[{"imageOffset":16232,"sourceLine":3,"sourceFile":"test.c","symbol":"foo","imageIndex":0,"symbolLocation":16},{"imageOffset":16256,"sourceLine":6,"sourceFile":"test.c","symbol":"bar","imageIndex":0,"symbolLocation":12},{"imageOffset":16288,"sourceLine":8,"sourceFile":"test.c","symbol":"main","imageIndex":0,"symbolLocation":24},{"imageOffset":20620,"symbol":"start","symbolLocation":520,"imageIndex":1}]}],
"usedImages" : [
{
"source" : "P",
"arch" : "arm64",
"base" : 4372692992,
"size" : 16384,
"uuid" : "b928ee77-9429-334f-ac88-41440bb3d4c7",
"path" : "\/Users\/USER\/*\/scripted_crashlog_json.test.tmp.out",
"name" : "scripted_crashlog_json.test.tmp.out"
},
{
"source" : "P",
"arch" : "arm64e",
"base" : 4372938752,
"size" : 393216,
"uuid" : "41293cda-474b-3700-924e-6ba0f7698eac",
"path" : "\/usr\/lib\/dyld",
"name" : "dyld"
}
],
"sharedCache" : {
"base" : 6924156928,
"size" : 3151052800,
"uuid" : "2ff78c31-e522-3e4a-a414-568e926f7274"
},
"vmSummary" : "ReadOnly portion of Libraries: Total=589.5M resident=0K(0%) swapped_out_or_unallocated=589.5M(100%)\nWritable regions: Total=529.1M written=0K(0%) resident=0K(0%) swapped_out=0K(0%) unallocated=529.1M(100%)\n\n VIRTUAL REGION \nREGION TYPE SIZE COUNT (non-coalesced) \n=========== ======= ======= \nKernel Alloc Once 32K 1 \nMALLOC 137.2M 11 \nMALLOC guard page 96K 5 \nMALLOC_NANO (reserved) 384.0M 1 reserved VM address space (unallocated)\nSTACK GUARD 56.0M 1 \nStack 8176K 1 \n__AUTH 46K 11 \n__AUTH_CONST 67K 38 \n__DATA 173K 36 \n__DATA_CONST 242K 39 \n__DATA_DIRTY 73K 21 \n__LINKEDIT 584.9M 3 \n__OBJC_CONST 10K 5 \n__OBJC_RO 82.9M 1 \n__OBJC_RW 3168K 1 \n__TEXT 4696K 43 \ndyld private memory 1024K 1 \nshared memory 48K 2 \n=========== ======= ======= \nTOTAL 1.2G 221 \nTOTAL, minus reserved VM space 878.5M 221 \n",
"legacyInfo" : {
"threadTriggered" : {
"queue" : "com.apple.main-thread"
}
},
"trialInfo" : { }
}

View file

@ -0,0 +1,10 @@
# RUN: %clang_host -g %S/Inputs/test.c -o %t.out
# RUN: cp %S/Inputs/scripted_crashlog.ips %t.crash
# RUN: %python %S/patch-crashlog.py --binary %t.out --crashlog %t.crash --offsets '{"main":20, "bar":9, "foo":16}' --json
# RUN: %lldb %t.out -o 'command script import lldb.macosx.crashlog' -o 'crashlog -i %t.crash' -o 'process status' 2>&1 | FileCheck %s
# CHECK: "crashlog" {{.*}} commands have been installed, use the "--help" options on these commands
# CHECK: Process 92190 stopped
# CHECK: * thread #1, name = 'CrashLogScriptedThread.thread-0', stop reason = EXC_BAD_ACCESS
# CHECK: frame #0: 0x0000000104a23f68 scripted_crashlog_json.test.tmp.out`foo at test.c:3:6 [artificial]