Start debugserver:
tty0 # debugserver localhost:8000 main
Start tracing:
tty1 $ lldb
tty1 (lldb) command script import trace.py
# Public domain. | |
# (lldb) command script import trace.py | |
from __future__ import print_function | |
import lldb | |
import sys | |
TARGET = None | |
END_ADDR = None | |
INVALID_ADDR = 0xffffffffffffffff | |
SHOULD_STOP = True | |
FILE_LOG = None | |
def log(s): | |
global FILE_LOG | |
sys.stdout.write(s) | |
sys.stdout.flush() | |
FILE_LOG.write(s) | |
FILE_LOG.flush() | |
def run(cmd): | |
res = lldb.SBCommandReturnObject() | |
lldb.debugger.GetCommandInterpreter().HandleCommand(cmd, res) | |
return res.GetOutput() | |
def function_address(fname): | |
global TARGET | |
# XXX: Multiple functions are not supported. | |
funcs = list(TARGET.FindFunctions(fname)) | |
for i in range(len(funcs)): | |
# XXX: Is this a good way to filter out inlined functions? | |
if funcs[i].GetSymbol().GetType() != 2: | |
del funcs[i] | |
assert len(funcs) == 1 | |
return funcs[0].GetFunction().GetStartAddress() | |
def full_name(obj): | |
return '{}.{}'.format(__name__, obj.__name__) | |
def log_trace(): | |
global TARGET | |
process = TARGET.GetProcess() | |
thread = process.GetSelectedThread() | |
frame = thread.GetSelectedFrame() | |
pc = frame.GetPCAddress() | |
pc_load_addr = pc.GetLoadAddress(TARGET) | |
func_name = pc.GetFunction().name | |
line_num = pc.GetLineEntry().line | |
insns = TARGET.ReadInstructions(pc, 1, 'intel') | |
insn = insns.GetInstructionAtIndex(0) | |
mnemonic = insn.mnemonic | |
operands = insn.operands | |
if func_name: | |
file_str = '{}; {}:{}'.format(' ' * 2, func_name, line_num) | |
else: | |
file_str = '' | |
log('0x{:016x}: {} {}{}\n' | |
.format(pc_load_addr, mnemonic, operands, file_str)) | |
def trace(frame, _bp_loc, _dict): | |
# Don't forget to trace the current instruction. | |
log_trace() | |
# Continue tracing. | |
# XXX: This seems to be the only way to start stepping from the callback. | |
run('thread step-scripted -C {}'.format(full_name(Trace))) | |
# XXX: Doesn't seem to matter with 'thread step-scripted'. | |
# return False # continue | |
return True # stop | |
class Trace: | |
def __init__(self, thread_plan, _dict): | |
global END_ADDR | |
global INVALID_ADDR | |
assert END_ADDR is not None | |
assert END_ADDR != INVALID_ADDR | |
self.thread_plan = thread_plan | |
self.end_addr = END_ADDR | |
def explains_stop(self, event): | |
stop_reason = self.thread_plan.GetThread().GetStopReason() | |
if stop_reason == lldb.eStopReasonTrace: | |
return True | |
else: | |
return False | |
def should_stop(self, event): | |
global SHOULD_STOP | |
global TARGET | |
log_trace() | |
process = TARGET.GetProcess() | |
thread = process.GetSelectedThread() | |
frame = thread.GetSelectedFrame() | |
pc = frame.GetPCAddress() | |
pc_load_addr = pc.GetLoadAddress(TARGET) | |
if pc_load_addr == self.end_addr: | |
self.thread_plan.SetPlanComplete(True) | |
return SHOULD_STOP | |
else: | |
return False | |
def should_step(self): | |
return True | |
def add_trace(start_addr, end_addr, should_stop): | |
global TARGET | |
global SHOULD_STOP | |
global END_ADDR | |
END_ADDR = end_addr | |
SHOULD_STOP = should_stop | |
breakpoint = TARGET.BreakpointCreateByAddress(start_addr) | |
breakpoint.SetScriptCallbackFunction(full_name(trace)) | |
def main(): | |
global TARGET | |
global FILE_LOG | |
FILE_LOG = open('/tmp/lldb.log', 'w') | |
# debugserver localhost:8000 main | |
run('gdb-remote 8000') | |
debugger = lldb.debugger | |
debugger.SetAsync(True) | |
TARGET = debugger.GetSelectedTarget() | |
TARGET.DeleteAllBreakpoints() | |
TARGET.DeleteAllWatchpoints() | |
main_addr = function_address('main') | |
main_addr = main_addr.GetLoadAddress(TARGET) | |
assert main_addr != INVALID_ADDR | |
start_addr = main_addr + 8 | |
end_addr = main_addr + 41 # ret | |
add_trace(start_addr, end_addr, should_stop=True) | |
# add_trace(start_addr, end_addr, should_stop=False) | |
run('cont') | |
def __lldb_init_module(_debugger, _dict): | |
main() |