GP-4415: Lots of lldb trace-rmi fixes

Breakpoint Enabled atribute.
Test fixes on macOS and Linux.
Re-work value conversion a bit.
shlexify commands.
Add method display names.
This commit is contained in:
Dan
2024-03-22 08:56:59 -04:00
parent 523f6e4cbe
commit eb5bf458a4
22 changed files with 1828 additions and 1222 deletions
@@ -1506,7 +1506,7 @@ def ghidra_trace_sync_disable(*, is_mi, **kwargs):
""" """
Cease synchronizing the current inferior with the Ghidra trace. Cease synchronizing the current inferior with the Ghidra trace.
This is the opposite of 'ghidra trace sync-disable', except it will not This is the opposite of 'ghidra trace sync-enable', except it will not
automatically remove hooks. automatically remove hooks.
""" """
@@ -24,7 +24,7 @@
#@menu-group local #@menu-group local
#@icon icon.debugger #@icon icon.debugger
#@help TraceRmiLauncherServicePlugin#lldb #@help TraceRmiLauncherServicePlugin#lldb
#@enum StartCmd:str run "process launch" "process launch --stop-at-entry" #@enum StartCmd:str "process launch" "process launch --stop-at-entry"
#@arg :str "Image" "The target binary executable image" #@arg :str "Image" "The target binary executable image"
#@args "Arguments" "Command-line arguments to pass to the target" #@args "Arguments" "Command-line arguments to pass to the target"
#@env OPT_LLDB_PATH:str="lldb" "Path to lldb" "The path to lldb. Omit the full path to resolve using the system PATH." #@env OPT_LLDB_PATH:str="lldb" "Path to lldb" "The path to lldb. Omit the full path to resolve using the system PATH."
@@ -50,8 +50,8 @@ language_map = {
'thumbv7em': ['ARM:BE:32:Cortex', 'ARM:LE:32:Cortex'], 'thumbv7em': ['ARM:BE:32:Cortex', 'ARM:LE:32:Cortex'],
'armv8': ['ARM:BE:32:v8', 'ARM:LE:32:v8'], 'armv8': ['ARM:BE:32:v8', 'ARM:LE:32:v8'],
'armv8l': ['ARM:BE:32:v8', 'ARM:LE:32:v8'], 'armv8l': ['ARM:BE:32:v8', 'ARM:LE:32:v8'],
'arm64': ['ARM:BE:64:v8', 'ARM:LE:64:v8'], 'arm64': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:AppleSilicon', 'AARCH64:LE:64:v8A'],
'arm64e': ['ARM:BE:64:v8', 'ARM:LE:64:v8'], 'arm64e': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:AppleSilicon', 'AARCH64:LE:64:v8A'],
'arm64_32': ['ARM:BE:32:v8', 'ARM:LE:32:v8'], 'arm64_32': ['ARM:BE:32:v8', 'ARM:LE:32:v8'],
'mips': ['MIPS:BE:32:default', 'MIPS:LE:32:default'], 'mips': ['MIPS:BE:32:default', 'MIPS:LE:32:default'],
'mipsr2': ['MIPS:BE:32:default', 'MIPS:LE:32:default'], 'mipsr2': ['MIPS:BE:32:default', 'MIPS:LE:32:default'],
@@ -141,10 +141,24 @@ compiler_map = {
} }
def get_arch(): def find_host_triple():
dbg = util.get_debugger()
for i in range(dbg.GetNumPlatforms()):
platform = dbg.GetPlatformAtIndex(i)
if platform.GetName() == 'host':
return platform.GetTriple()
return 'unrecognized'
def find_triple():
triple = util.get_target().triple triple = util.get_target().triple
if triple is None: if triple is not None:
return "x86_64" return triple
return find_host_triple()
def get_arch():
triple = find_triple()
return triple.split('-')[0] return triple.split('-')[0]
@@ -152,7 +166,6 @@ def get_endian():
parm = util.get_convenience_variable('endian') parm = util.get_convenience_variable('endian')
if parm != 'auto': if parm != 'auto':
return parm return parm
# Once again, we have to hack using the human-readable 'show'
order = util.get_target().GetByteOrder() order = util.get_target().GetByteOrder()
if order is lldb.eByteOrderLittle: if order is lldb.eByteOrderLittle:
return 'little' return 'little'
@@ -167,15 +180,11 @@ def get_osabi():
parm = util.get_convenience_variable('osabi') parm = util.get_convenience_variable('osabi')
if not parm in ['auto', 'default']: if not parm in ['auto', 'default']:
return parm return parm
# We have to hack around the fact the LLDB won't give us the current OS ABI triple = find_triple()
# via the API if it is "auto" or "default". Using "show", we can get it, but
# we have to parse output meant for a human. The current value will be on
# the top line, delimited by double quotes. It will be the last delimited
# thing on that line. ("auto" may appear earlier on the line.)
triple = util.get_target().triple
# this is an unfortunate feature of the tests # this is an unfortunate feature of the tests
if triple is None or '-' not in triple: if triple is None or '-' not in triple:
return "default" return "default"
triple = find_triple()
return triple.split('-')[2] return triple.split('-')[2]
@@ -274,29 +283,8 @@ class DefaultRegisterMapper(object):
def map_name(self, proc, name): def map_name(self, proc, name):
return name return name
"""
def convert_value(self, value, type=None):
if type is None:
type = value.dynamic_type.strip_typedefs()
l = type.sizeof
# l - 1 because array() takes the max index, inclusive
# NOTE: Might like to pre-lookup 'unsigned char', but it depends on the
# architecture *at the time of lookup*.
cv = value.cast(lldb.lookup_type('unsigned char').array(l - 1))
rng = range(l)
if self.byte_order == 'little':
rng = reversed(rng)
return bytes(cv[i] for i in rng)
"""
def map_value(self, proc, name, value): def map_value(self, proc, name, value):
try: return RegVal(self.map_name(proc, name), value)
# TODO: this seems half-baked
av = value.to_bytes(8, "big")
except e:
raise ValueError("Cannot convert {}'s value: '{}', type: '{}'"
.format(name, value, value.type))
return RegVal(self.map_name(proc, name), av)
def map_name_back(self, proc, name): def map_name_back(self, proc, name):
return name return name
File diff suppressed because it is too large Load Diff
@@ -67,13 +67,22 @@ class ProcessState(object):
hashable_frame = (thread.GetThreadID(), frame.GetFrameID()) hashable_frame = (thread.GetThreadID(), frame.GetFrameID())
if first or hashable_frame not in self.visited: if first or hashable_frame not in self.visited:
banks = frame.GetRegisters() banks = frame.GetRegisters()
primary = banks.GetFirstValueByName(commands.DEFAULT_REGISTER_BANK) primary = banks.GetFirstValueByName(
commands.DEFAULT_REGISTER_BANK)
if primary.value is None: if primary.value is None:
primary = banks[0] primary = banks[0]
commands.DEFAULT_REGISTER_BANK = primary.name if primary is not None:
commands.putreg(frame, primary) commands.DEFAULT_REGISTER_BANK = primary.name
commands.putmem("$pc", "1", result=None) if primary is not None:
commands.putmem("$sp", "1", result=None) commands.putreg(frame, primary)
try:
commands.putmem("$pc", "1", result=None)
except BaseException as e:
print(f"Couldn't record page with PC: {e}")
try:
commands.putmem("$sp", "1", result=None)
except BaseException as e:
print(f"Couldn't record page with SP: {e}")
self.visited.add(hashable_frame) self.visited.add(hashable_frame)
if first or self.regions or self.threads or self.modules: if first or self.regions or self.threads or self.modules:
# Sections, memory syscalls, or stack allocations # Sections, memory syscalls, or stack allocations
@@ -113,7 +122,7 @@ class BrkState(object):
return self.break_loc_counts.get(b.GetID(), 0) return self.break_loc_counts.get(b.GetID(), 0)
def del_brkloc_count(self, b): def del_brkloc_count(self, b):
if b not in self.break_loc_counts: if b.GetID() not in self.break_loc_counts:
return 0 # TODO: Print a warning? return 0 # TODO: Print a warning?
count = self.break_loc_counts[b.GetID()] count = self.break_loc_counts[b.GetID()]
del self.break_loc_counts[b.GetID()] del self.break_loc_counts[b.GetID()]
@@ -125,19 +134,32 @@ BRK_STATE = BrkState()
PROC_STATE = {} PROC_STATE = {}
class QuitSentinel(object):
pass
QUIT = QuitSentinel()
def process_event(self, listener, event): def process_event(self, listener, event):
try: try:
desc = util.get_description(event) desc = util.get_description(event)
# print('Event:', desc) # print(f"Event: {desc}")
target = util.get_target()
if not target.IsValid():
# LLDB may crash on event.GetBroadcasterClass, otherwise
# All the checks below, e.g. SBTarget.EventIsTargetEvent, call this
print(f"Ignoring {desc} because target is invalid")
return
event_process = util.get_process() event_process = util.get_process()
if event_process not in PROC_STATE: if event_process.IsValid() and event_process not in PROC_STATE:
PROC_STATE[event_process.GetProcessID()] = ProcessState() PROC_STATE[event_process.GetProcessID()] = ProcessState()
rc = event_process.GetBroadcaster().AddListener(listener, ALL_EVENTS) rc = event_process.GetBroadcaster().AddListener(listener, ALL_EVENTS)
if rc is False: if not rc:
print("add listener for process failed") print("add listener for process failed")
# NB: Calling put_state on running leaves an open transaction # NB: Calling put_state on running leaves an open transaction
if event_process.is_running is False: if not event_process.is_running:
commands.put_state(event_process) commands.put_state(event_process)
type = event.GetType() type = event.GetType()
if lldb.SBTarget.EventIsTargetEvent(event): if lldb.SBTarget.EventIsTargetEvent(event):
@@ -157,6 +179,8 @@ def process_event(self, listener, event):
return on_exited(event) return on_exited(event)
if event_process.is_stopped: if event_process.is_stopped:
return on_stop(event) return on_stop(event)
if event_process.is_running:
return on_cont(event)
return True return True
if (type & lldb.SBProcess.eBroadcastBitInterrupt) != 0: if (type & lldb.SBProcess.eBroadcastBitInterrupt) != 0:
if event_process.is_stopped: if event_process.is_stopped:
@@ -244,6 +268,9 @@ def process_event(self, listener, event):
if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousOutputData) != 0: if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousOutputData) != 0:
return True return True
if (type & lldb.SBCommandInterpreter.eBroadcastBitQuitCommandReceived) != 0: if (type & lldb.SBCommandInterpreter.eBroadcastBitQuitCommandReceived) != 0:
# DO NOT return QUIT here.
# For some reason, this event comes just after launch.
# Maybe need to figure out *which* interpreter?
return True return True
if (type & lldb.SBCommandInterpreter.eBroadcastBitResetPrompt) != 0: if (type & lldb.SBCommandInterpreter.eBroadcastBitResetPrompt) != 0:
return True return True
@@ -251,7 +278,7 @@ def process_event(self, listener, event):
return True return True
print("UNKNOWN EVENT") print("UNKNOWN EVENT")
return True return True
except RuntimeError as e: except BaseException as e:
print(e) print(e)
@@ -267,50 +294,57 @@ class EventThread(threading.Thread):
target = util.get_target() target = util.get_target()
proc = util.get_process() proc = util.get_process()
rc = cli.GetBroadcaster().AddListener(listener, ALL_EVENTS) rc = cli.GetBroadcaster().AddListener(listener, ALL_EVENTS)
if rc is False: if not rc:
print("add listener for cli failed") print("add listener for cli failed")
return # return
rc = target.GetBroadcaster().AddListener(listener, ALL_EVENTS) rc = target.GetBroadcaster().AddListener(listener, ALL_EVENTS)
if rc is False: if not rc:
print("add listener for target failed") print("add listener for target failed")
return # return
rc = proc.GetBroadcaster().AddListener(listener, ALL_EVENTS) rc = proc.GetBroadcaster().AddListener(listener, ALL_EVENTS)
if rc is False: if not rc:
print("add listener for process failed") print("add listener for process failed")
return # return
# Not sure what effect this logic has # Not sure what effect this logic has
rc = cli.GetBroadcaster().AddInitialEventsToListener(listener, ALL_EVENTS) rc = cli.GetBroadcaster().AddInitialEventsToListener(listener, ALL_EVENTS)
if rc is False: if not rc:
print("add listener for cli failed") print("add initial events for cli failed")
return # return
rc = target.GetBroadcaster().AddInitialEventsToListener(listener, ALL_EVENTS) rc = target.GetBroadcaster().AddInitialEventsToListener(listener, ALL_EVENTS)
if rc is False: if not rc:
print("add listener for target failed") print("add initial events for target failed")
return # return
rc = proc.GetBroadcaster().AddInitialEventsToListener(listener, ALL_EVENTS) rc = proc.GetBroadcaster().AddInitialEventsToListener(listener, ALL_EVENTS)
if rc is False: if not rc:
print("add listener for process failed") print("add initial events for process failed")
return # return
rc = listener.StartListeningForEventClass( rc = listener.StartListeningForEventClass(
util.get_debugger(), lldb.SBThread.GetBroadcasterClassName(), ALL_EVENTS) util.get_debugger(), lldb.SBThread.GetBroadcasterClassName(), ALL_EVENTS)
if rc is False: if not rc:
print("add listener for threads failed") print("add listener for threads failed")
return # return
# THIS WILL NOT WORK: listener = util.get_debugger().GetListener() # THIS WILL NOT WORK: listener = util.get_debugger().GetListener()
while True: while True:
event_recvd = False event_recvd = False
while event_recvd is False: while not event_recvd:
if listener.WaitForEvent(lldb.UINT32_MAX, self.event): if listener.WaitForEvent(lldb.UINT32_MAX, self.event):
try: try:
self.func(listener, self.event) result = self.func(listener, self.event)
while listener.GetNextEvent(self.event): if result is QUIT:
self.func(listener, self.event) return
event_recvd = True
except BaseException as e: except BaseException as e:
print(e) print(e)
while listener.GetNextEvent(self.event):
try:
result = self.func(listener, self.event)
if result is QUIT:
return
except BaseException as e:
print(e)
event_recvd = True
proc = util.get_process() proc = util.get_process()
if proc is not None and not proc.is_alive: if proc is not None and not proc.is_alive:
break break
@@ -445,7 +479,7 @@ def on_memory_changed(event):
def on_register_changed(event): def on_register_changed(event):
print("Register changed: {}".format(dir(event))) # print("Register changed: {}".format(dir(event)))
proc = util.get_process() proc = util.get_process()
if proc.GetProcessID() not in PROC_STATE: if proc.GetProcessID() not in PROC_STATE:
return return
@@ -476,7 +510,8 @@ def on_cont(event):
def on_stop(event): def on_stop(event):
proc = lldb.SBProcess.GetProcessFromEvent(event) if event is not None else util.get_process() proc = lldb.SBProcess.GetProcessFromEvent(
event) if event is not None else util.get_process()
if proc.GetProcessID() not in PROC_STATE: if proc.GetProcessID() not in PROC_STATE:
print("not in state") print("not in state")
return return
@@ -586,7 +621,7 @@ def on_breakpoint_deleted(b):
notify_others_breaks(proc) notify_others_breaks(proc)
if proc.GetProcessID() not in PROC_STATE: if proc.GetProcessID() not in PROC_STATE:
return return
old_count = BRK_STATE.del_brkloc_count(b.GetID()) old_count = BRK_STATE.del_brkloc_count(b)
trace = commands.STATE.trace trace = commands.STATE.trace
if trace is None: if trace is None:
return return
@@ -15,9 +15,11 @@
## ##
from concurrent.futures import Future, ThreadPoolExecutor from concurrent.futures import Future, ThreadPoolExecutor
import re import re
import sys
from ghidratrace import sch from ghidratrace import sch
from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange
import lldb import lldb
from . import commands, util from . import commands, util
@@ -228,107 +230,122 @@ def find_bpt_loc_by_obj(object):
return bpt.locations[locnum - 1] # Display is 1-up return bpt.locations[locnum - 1] # Display is 1-up
def exec_convert_errors(cmd, to_string=False):
res = lldb.SBCommandReturnObject()
util.get_debugger().GetCommandInterpreter().HandleCommand(cmd, res)
if not res.Succeeded():
if not to_string:
print(res.GetError(), file=sys.stderr)
raise RuntimeError(res.GetError())
if to_string:
return res.GetOutput()
print(res.GetOutput(), end="")
@REGISTRY.method @REGISTRY.method
def execute(cmd: str, to_string: bool=False): def execute(cmd: str, to_string: bool=False):
"""Execute a CLI command.""" """Execute a CLI command."""
res = lldb.SBCommandReturnObject() # TODO: Check for eCommandInterpreterResultQuitRequested?
util.get_debugger().GetCommandInterpreter().HandleCommand(cmd, res) return exec_convert_errors(cmd, to_string)
if to_string:
if res.Succeeded():
return res.GetOutput()
else:
return res.GetError()
@REGISTRY.method(action='refresh') @REGISTRY.method
def evaluate(expr: str):
"""Evaluate an expression."""
value = util.get_target().EvaluateExpression(expr)
if value.GetError().Fail():
raise RuntimeError(value.GetError().GetCString())
return commands.convert_value(value)
@REGISTRY.method
def pyeval(expr: str):
return eval(expr)
@REGISTRY.method(action='refresh', display="Refresh Available")
def refresh_available(node: sch.Schema('AvailableContainer')): def refresh_available(node: sch.Schema('AvailableContainer')):
"""List processes on lldb's host system.""" """List processes on lldb's host system."""
with commands.open_tracked_tx('Refresh Available'): with commands.open_tracked_tx('Refresh Available'):
util.get_debugger().HandleCommand('ghidra trace put-available') exec_convert_errors('ghidra trace put-available')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh', display="Refresh Breakpoints")
def refresh_breakpoints(node: sch.Schema('BreakpointContainer')): def refresh_breakpoints(node: sch.Schema('BreakpointContainer')):
""" """
Refresh the list of breakpoints (including locations for the current Refresh the list of breakpoints (including locations for the current
process). process).
""" """
with commands.open_tracked_tx('Refresh Breakpoints'): with commands.open_tracked_tx('Refresh Breakpoints'):
util.get_debugger().HandleCommand('ghidra trace put-breakpoints') exec_convert_errors('ghidra trace put-breakpoints')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh', display="Refresh Processes")
def refresh_processes(node: sch.Schema('ProcessContainer')): def refresh_processes(node: sch.Schema('ProcessContainer')):
"""Refresh the list of processes.""" """Refresh the list of processes."""
with commands.open_tracked_tx('Refresh Processes'): with commands.open_tracked_tx('Refresh Processes'):
util.get_debugger().HandleCommand('ghidra trace put-threads') exec_convert_errors('ghidra trace put-threads')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh', display="Refresh Breakpoints")
def refresh_proc_breakpoints(node: sch.Schema('BreakpointLocationContainer')): def refresh_proc_breakpoints(node: sch.Schema('BreakpointLocationContainer')):
""" """
Refresh the breakpoint locations for the process. Refresh the breakpoints for the process.
In the course of refreshing the locations, the breakpoint list will also be
refreshed.
""" """
with commands.open_tracked_tx('Refresh Breakpoint Locations'): with commands.open_tracked_tx('Refresh Breakpoint Locations'):
util.get_debugger().HandleCommand('ghidra trace put-breakpoints') exec_convert_errors('ghidra trace put-breakpoints')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh', display="Refresh Watchpoints")
def refresh_proc_watchpoints(node: sch.Schema('WatchpointContainer')): def refresh_proc_watchpoints(node: sch.Schema('WatchpointContainer')):
""" """
Refresh the watchpoint locations for the process. Refresh the watchpoints for the process.
In the course of refreshing the locations, the watchpoint list will also be
refreshed.
""" """
with commands.open_tracked_tx('Refresh Watchpoint Locations'): with commands.open_tracked_tx('Refresh Watchpoint Locations'):
util.get_debugger().HandleCommand('ghidra trace put-watchpoints') exec_convert_errors('ghidra trace put-watchpoints')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh', display="Refresh Environment")
def refresh_environment(node: sch.Schema('Environment')): def refresh_environment(node: sch.Schema('Environment')):
"""Refresh the environment descriptors (arch, os, endian).""" """Refresh the environment descriptors (arch, os, endian)."""
with commands.open_tracked_tx('Refresh Environment'): with commands.open_tracked_tx('Refresh Environment'):
util.get_debugger().HandleCommand('ghidra trace put-environment') exec_convert_errors('ghidra trace put-environment')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh', display="Refresh Threads")
def refresh_threads(node: sch.Schema('ThreadContainer')): def refresh_threads(node: sch.Schema('ThreadContainer')):
"""Refresh the list of threads in the process.""" """Refresh the list of threads in the process."""
with commands.open_tracked_tx('Refresh Threads'): with commands.open_tracked_tx('Refresh Threads'):
util.get_debugger().HandleCommand('ghidra trace put-threads') exec_convert_errors('ghidra trace put-threads')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh', display="Refresh Stack")
def refresh_stack(node: sch.Schema('Stack')): def refresh_stack(node: sch.Schema('Stack')):
"""Refresh the backtrace for the thread.""" """Refresh the backtrace for the thread."""
t = find_thread_by_stack_obj(node) t = find_thread_by_stack_obj(node)
t.process.SetSelectedThread(t) t.process.SetSelectedThread(t)
with commands.open_tracked_tx('Refresh Stack'): with commands.open_tracked_tx('Refresh Stack'):
util.get_debugger().HandleCommand('ghidra trace put-frames') exec_convert_errors('ghidra trace put-frames')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh', display="Refresh Registers")
def refresh_registers(node: sch.Schema('RegisterValueContainer')): def refresh_registers(node: sch.Schema('RegisterValueContainer')):
"""Refresh the register values for the frame.""" """Refresh the register values for the frame."""
f = find_frame_by_regs_obj(node) f = find_frame_by_regs_obj(node)
f.thread.SetSelectedFrame(f.GetFrameID()) f.thread.SetSelectedFrame(f.GetFrameID())
# TODO: Groups? # TODO: Groups?
with commands.open_tracked_tx('Refresh Registers'): with commands.open_tracked_tx('Refresh Registers'):
util.get_debugger().HandleCommand('ghidra trace putreg') exec_convert_errors('ghidra trace putreg')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh', display="Refresh Memory")
def refresh_mappings(node: sch.Schema('Memory')): def refresh_mappings(node: sch.Schema('Memory')):
"""Refresh the list of memory regions for the process.""" """Refresh the list of memory regions for the process."""
with commands.open_tracked_tx('Refresh Memory Regions'): with commands.open_tracked_tx('Refresh Memory Regions'):
util.get_debugger().HandleCommand('ghidra trace put-regions') exec_convert_errors('ghidra trace put-regions')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh', display="Refresh Modules")
def refresh_modules(node: sch.Schema('ModuleContainer')): def refresh_modules(node: sch.Schema('ModuleContainer')):
""" """
Refresh the modules and sections list for the process. Refresh the modules and sections list for the process.
@@ -336,12 +353,13 @@ def refresh_modules(node: sch.Schema('ModuleContainer')):
This will refresh the sections for all modules, not just the selected one. This will refresh the sections for all modules, not just the selected one.
""" """
with commands.open_tracked_tx('Refresh Modules'): with commands.open_tracked_tx('Refresh Modules'):
util.get_debugger().HandleCommand('ghidra trace put-modules') exec_convert_errors('ghidra trace put-modules')
@REGISTRY.method(action='activate') @REGISTRY.method(action='activate')
def activate_process(process: sch.Schema('Process')): def activate_process(process: sch.Schema('Process')):
"""Switch to the process.""" """Switch to the process."""
# TODO
return return
@@ -363,41 +381,48 @@ def activate_frame(frame: sch.Schema('StackFrame')):
def remove_process(process: sch.Schema('Process')): def remove_process(process: sch.Schema('Process')):
"""Remove the process.""" """Remove the process."""
proc = find_proc_by_obj(process) proc = find_proc_by_obj(process)
util.get_debugger().HandleCommand(f'target delete 0') exec_convert_errors(f'target delete 0')
@REGISTRY.method(action='connect') @REGISTRY.method(action='connect', display="Connect Target")
def target(process: sch.Schema('Process'), spec: str): def target(process: sch.Schema('Process'), spec: str):
"""Connect to a target machine or process.""" """Connect to a target machine or process."""
util.get_debugger().HandleCommand(f'target select {spec}') exec_convert_errors(f'target select {spec}')
@REGISTRY.method(action='attach') @REGISTRY.method(action='attach', display="Attach by Attachable")
def attach_obj(process: sch.Schema('Process'), target: sch.Schema('Attachable')): def attach_obj(process: sch.Schema('Process'), target: sch.Schema('Attachable')):
"""Attach the process to the given target.""" """Attach the process to the given target."""
pid = find_availpid_by_obj(target) pid = find_availpid_by_obj(target)
util.get_debugger().HandleCommand(f'process attach -p {pid}') exec_convert_errors(f'process attach -p {pid}')
@REGISTRY.method(action='attach') @REGISTRY.method(action='attach', display="Attach by PID")
def attach_pid(process: sch.Schema('Process'), pid: int): def attach_pid(process: sch.Schema('Process'), pid: int):
"""Attach the process to the given target.""" """Attach the process to the given target."""
util.get_debugger().HandleCommand(f'process attach -p {pid}') exec_convert_errors(f'process attach -p {pid}')
@REGISTRY.method(action='attach') @REGISTRY.method(action='attach', display="Attach by Name")
def attach_name(process: sch.Schema('Process'), name: str): def attach_name(process: sch.Schema('Process'), name: str):
"""Attach the process to the given target.""" """Attach the process to the given target."""
util.get_debugger().HandleCommand(f'process attach -n {name}') exec_convert_errors(f'process attach -n {name}')
@REGISTRY.method @REGISTRY.method(display="Detach")
def detach(process: sch.Schema('Process')): def detach(process: sch.Schema('Process')):
"""Detach the process's target.""" """Detach the process's target."""
util.get_debugger().HandleCommand(f'process detach') exec_convert_errors(f'process detach')
@REGISTRY.method(action='launch') def do_launch(process, file, args, cmd):
exec_convert_errors(f'file {file}')
if args != '':
exec_convert_errors(f'settings set target.run-args {args}')
exec_convert_errors(cmd)
@REGISTRY.method(action='launch', display="Launch at Entry")
def launch_loader(process: sch.Schema('Process'), def launch_loader(process: sch.Schema('Process'),
file: ParamDesc(str, display='File'), file: ParamDesc(str, display='File'),
args: ParamDesc(str, display='Arguments')=''): args: ParamDesc(str, display='Arguments')=''):
@@ -406,14 +431,10 @@ def launch_loader(process: sch.Schema('Process'),
If 'main' is not defined in the file, this behaves like 'run'. If 'main' is not defined in the file, this behaves like 'run'.
""" """
util.get_debugger().HandleCommand(f'file {file}') do_launch(process, file, args, 'process launch --stop-at-entry')
if args != '':
util.get_debugger().HandleCommand(
f'settings set target.run-args {args}')
util.get_debugger().HandleCommand(f'process launch --stop-at-entry')
@REGISTRY.method(action='launch') @REGISTRY.method(action='launch', display="Launch and Run")
def launch(process: sch.Schema('Process'), def launch(process: sch.Schema('Process'),
file: ParamDesc(str, display='File'), file: ParamDesc(str, display='File'),
args: ParamDesc(str, display='Arguments')=''): args: ParamDesc(str, display='Arguments')=''):
@@ -423,31 +444,27 @@ def launch(process: sch.Schema('Process'),
The process will not stop until it hits one of your breakpoints, or it is The process will not stop until it hits one of your breakpoints, or it is
signaled. signaled.
""" """
util.get_debugger().HandleCommand(f'file {file}') do_launch(process, file, args, 'run')
if args != '':
util.get_debugger().HandleCommand(
f'settings set target.run-args {args}')
util.get_debugger().HandleCommand(f'run')
@REGISTRY.method @REGISTRY.method
def kill(process: sch.Schema('Process')): def kill(process: sch.Schema('Process')):
"""Kill execution of the process.""" """Kill execution of the process."""
util.get_debugger().HandleCommand('process kill') exec_convert_errors('process kill')
@REGISTRY.method(name='continue', action='resume') @REGISTRY.method(name='continue', action='resume')
def _continue(process: sch.Schema('Process')): def _continue(process: sch.Schema('Process')):
"""Continue execution of the process.""" """Continue execution of the process."""
util.get_debugger().HandleCommand('process continue') exec_convert_errors('process continue')
@REGISTRY.method @REGISTRY.method
def interrupt(): def interrupt():
"""Interrupt the execution of the debugged program.""" """Interrupt the execution of the debugged program."""
util.get_debugger().HandleCommand('process interrupt') exec_convert_errors('process interrupt')
# util.get_process().SendAsyncInterrupt() # util.get_process().SendAsyncInterrupt()
# util.get_debugger().HandleCommand('^c') # exec_convert_errors('^c')
# util.get_process().Signal(2) # util.get_process().Signal(2)
@@ -456,7 +473,7 @@ def step_into(thread: sch.Schema('Thread'), n: ParamDesc(int, display='N')=1):
"""Step on instruction exactly.""" """Step on instruction exactly."""
t = find_thread_by_obj(thread) t = find_thread_by_obj(thread)
t.process.SetSelectedThread(t) t.process.SetSelectedThread(t)
util.get_debugger().HandleCommand('thread step-inst') exec_convert_errors('thread step-inst')
@REGISTRY.method(action='step_over') @REGISTRY.method(action='step_over')
@@ -464,7 +481,7 @@ def step_over(thread: sch.Schema('Thread'), n: ParamDesc(int, display='N')=1):
"""Step one instruction, but proceed through subroutine calls.""" """Step one instruction, but proceed through subroutine calls."""
t = find_thread_by_obj(thread) t = find_thread_by_obj(thread)
t.process.SetSelectedThread(t) t.process.SetSelectedThread(t)
util.get_debugger().HandleCommand('thread step-inst-over') exec_convert_errors('thread step-inst-over')
@REGISTRY.method(action='step_out') @REGISTRY.method(action='step_out')
@@ -473,27 +490,27 @@ def step_out(thread: sch.Schema('Thread')):
if thread is not None: if thread is not None:
t = find_thread_by_obj(thread) t = find_thread_by_obj(thread)
t.process.SetSelectedThread(t) t.process.SetSelectedThread(t)
util.get_debugger().HandleCommand('thread step-out') exec_convert_errors('thread step-out')
@REGISTRY.method(action='step_ext') @REGISTRY.method(action='step_ext', display="Advance")
def step_ext(thread: sch.Schema('Thread'), address: Address): def step_advance(thread: sch.Schema('Thread'), address: Address):
"""Continue execution up to the given address.""" """Continue execution up to the given address."""
t = find_thread_by_obj(thread) t = find_thread_by_obj(thread)
t.process.SetSelectedThread(t) t.process.SetSelectedThread(t)
offset = thread.trace.memory_mapper.map_back(t.process, address) offset = thread.trace.memory_mapper.map_back(t.process, address)
util.get_debugger().HandleCommand(f'thread until -a {offset}') exec_convert_errors(f'thread until -a {offset}')
@REGISTRY.method(name='return', action='step_ext') @REGISTRY.method(action='step_ext', display="Return")
def _return(thread: sch.Schema('Thread'), value: int=None): def step_return(thread: sch.Schema('Thread'), value: int=None):
"""Skip the remainder of the current function.""" """Skip the remainder of the current function."""
t = find_thread_by_obj(thread) t = find_thread_by_obj(thread)
t.process.SetSelectedThread(t) t.process.SetSelectedThread(t)
if value is None: if value is None:
util.get_debugger().HandleCommand('thread return') exec_convert_errors('thread return')
else: else:
util.get_debugger().HandleCommand(f'thread return {value}') exec_convert_errors(f'thread return {value}')
@REGISTRY.method(action='break_sw_execute') @REGISTRY.method(action='break_sw_execute')
@@ -501,14 +518,14 @@ def break_address(process: sch.Schema('Process'), address: Address):
"""Set a breakpoint.""" """Set a breakpoint."""
proc = find_proc_by_obj(process) proc = find_proc_by_obj(process)
offset = process.trace.memory_mapper.map_back(proc, address) offset = process.trace.memory_mapper.map_back(proc, address)
util.get_debugger().HandleCommand(f'breakpoint set -a 0x{offset:x}') exec_convert_errors(f'breakpoint set -a 0x{offset:x}')
@REGISTRY.method(action='break_sw_execute') @REGISTRY.method(action='break_sw_execute')
def break_expression(expression: str): def break_expression(expression: str):
"""Set a breakpoint.""" """Set a breakpoint."""
# TODO: Escape? # TODO: Escape?
util.get_debugger().HandleCommand(f'breakpoint set -r {expression}') exec_convert_errors(f'breakpoint set -r {expression}')
@REGISTRY.method(action='break_hw_execute') @REGISTRY.method(action='break_hw_execute')
@@ -516,14 +533,14 @@ def break_hw_address(process: sch.Schema('Process'), address: Address):
"""Set a hardware-assisted breakpoint.""" """Set a hardware-assisted breakpoint."""
proc = find_proc_by_obj(process) proc = find_proc_by_obj(process)
offset = process.trace.memory_mapper.map_back(proc, address) offset = process.trace.memory_mapper.map_back(proc, address)
util.get_debugger().HandleCommand(f'breakpoint set -H -a 0x{offset:x}') exec_convert_errors(f'breakpoint set -H -a 0x{offset:x}')
@REGISTRY.method(action='break_hw_execute') @REGISTRY.method(action='break_hw_execute')
def break_hw_expression(expression: str): def break_hw_expression(expression: str):
"""Set a hardware-assisted breakpoint.""" """Set a hardware-assisted breakpoint."""
# TODO: Escape? # TODO: Escape?
util.get_debugger().HandleCommand(f'breakpoint set -H -name {expression}') exec_convert_errors(f'breakpoint set -H -name {expression}')
@REGISTRY.method(action='break_read') @REGISTRY.method(action='break_read')
@@ -533,15 +550,16 @@ def break_read_range(process: sch.Schema('Process'), range: AddressRange):
offset_start = process.trace.memory_mapper.map_back( offset_start = process.trace.memory_mapper.map_back(
proc, Address(range.space, range.min)) proc, Address(range.space, range.min))
sz = range.length() sz = range.length()
util.get_debugger().HandleCommand( exec_convert_errors(
f'watchpoint set expression -s {sz} -w read -- {offset_start}') f'watchpoint set expression -s {sz} -w read -- {offset_start}')
@REGISTRY.method(action='break_read') @REGISTRY.method(action='break_read')
def break_read_expression(expression: str): def break_read_expression(expression: str, size=None):
"""Set a read watchpoint.""" """Set a read watchpoint."""
util.get_debugger().HandleCommand( size_part = '' if size is None else f'-s {size}'
f'watchpoint set expression -w read -- {expression}') exec_convert_errors(
f'watchpoint set expression {size_part} -w read -- {expression}')
@REGISTRY.method(action='break_write') @REGISTRY.method(action='break_write')
@@ -551,15 +569,16 @@ def break_write_range(process: sch.Schema('Process'), range: AddressRange):
offset_start = process.trace.memory_mapper.map_back( offset_start = process.trace.memory_mapper.map_back(
proc, Address(range.space, range.min)) proc, Address(range.space, range.min))
sz = range.length() sz = range.length()
util.get_debugger().HandleCommand( exec_convert_errors(
f'watchpoint set expression -s {sz} -- {offset_start}') f'watchpoint set expression -s {sz} -- {offset_start}')
@REGISTRY.method(action='break_write') @REGISTRY.method(action='break_write')
def break_write_expression(expression: str): def break_write_expression(expression: str, size=None):
"""Set a watchpoint.""" """Set a watchpoint."""
util.get_debugger().HandleCommand( size_part = '' if size is None else f'-s {size}'
f'watchpoint set expression -- {expression}') exec_convert_errors(
f'watchpoint set expression {size_part} -- {expression}')
@REGISTRY.method(action='break_access') @REGISTRY.method(action='break_access')
@@ -569,21 +588,22 @@ def break_access_range(process: sch.Schema('Process'), range: AddressRange):
offset_start = process.trace.memory_mapper.map_back( offset_start = process.trace.memory_mapper.map_back(
proc, Address(range.space, range.min)) proc, Address(range.space, range.min))
sz = range.length() sz = range.length()
util.get_debugger().HandleCommand( exec_convert_errors(
f'watchpoint set expression -s {sz} -w read_write -- {offset_start}') f'watchpoint set expression -s {sz} -w read_write -- {offset_start}')
@REGISTRY.method(action='break_access') @REGISTRY.method(action='break_access')
def break_access_expression(expression: str): def break_access_expression(expression: str, size=None):
"""Set an access watchpoint.""" """Set an access watchpoint."""
util.get_debugger().HandleCommand( size_part = '' if size is None else f'-s {size}'
f'watchpoint set expression -w read_write -- {expression}') exec_convert_errors(
f'watchpoint set expression {size_part} -w read_write -- {expression}')
@REGISTRY.method(action='break_ext') @REGISTRY.method(action='break_ext', display="Break on Exception")
def break_exception(lang: str): def break_exception(lang: str):
"""Set a catchpoint.""" """Set a catchpoint."""
util.get_debugger().HandleCommand(f'breakpoint set -E {lang}') exec_convert_errors(f'breakpoint set -E {lang}')
@REGISTRY.method(action='toggle') @REGISTRY.method(action='toggle')
@@ -605,7 +625,7 @@ def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabl
"""Toggle a breakpoint location.""" """Toggle a breakpoint location."""
bptnum, locnum = find_bptlocnum_by_obj(location) bptnum, locnum = find_bptlocnum_by_obj(location)
cmd = 'enable' if enabled else 'disable' cmd = 'enable' if enabled else 'disable'
util.get_debugger().HandleCommand(f'breakpoint {cmd} {bptnum}.{locnum}') exec_convert_errors(f'breakpoint {cmd} {bptnum}.{locnum}')
@REGISTRY.method(action='delete') @REGISTRY.method(action='delete')
@@ -613,7 +633,7 @@ def delete_watchpoint(watchpoint: sch.Schema('WatchpointSpec')):
"""Delete a watchpoint.""" """Delete a watchpoint."""
wpt = find_wpt_by_obj(watchpoint) wpt = find_wpt_by_obj(watchpoint)
wptnum = wpt.GetID() wptnum = wpt.GetID()
util.get_debugger().HandleCommand(f'watchpoint delete {wptnum}') exec_convert_errors(f'watchpoint delete {wptnum}')
@REGISTRY.method(action='delete') @REGISTRY.method(action='delete')
@@ -621,7 +641,7 @@ def delete_breakpoint(breakpoint: sch.Schema('BreakpointSpec')):
"""Delete a breakpoint.""" """Delete a breakpoint."""
bpt = find_bpt_by_obj(breakpoint) bpt = find_bpt_by_obj(breakpoint)
bptnum = bpt.GetID() bptnum = bpt.GetID()
util.get_debugger().HandleCommand(f'breakpoint delete {bptnum}') exec_convert_errors(f'breakpoint delete {bptnum}')
@REGISTRY.method @REGISTRY.method
@@ -638,7 +658,7 @@ def read_mem(process: sch.Schema('Process'), range: AddressRange):
if result.Succeeded(): if result.Succeeded():
return return
print(f"Could not read 0x{offset_start:x}: {result}") print(f"Could not read 0x{offset_start:x}: {result}")
util.get_debugger().HandleCommand( exec_convert_errors(
f'ghidra trace putmem-state 0x{offset_start:x} {range.length()} error') f'ghidra trace putmem-state 0x{offset_start:x} {range.length()} error')
@@ -660,5 +680,5 @@ def write_reg(frame: sch.Schema('StackFrame'), name: str, value: bytes):
reg = find_reg_by_name(f, mname) reg = find_reg_by_name(f, mname)
size = int(lldb.parse_and_eval(f'sizeof(${mname})')) size = int(lldb.parse_and_eval(f'sizeof(${mname})'))
arr = '{' + ','.join(str(b) for b in mval) + '}' arr = '{' + ','.join(str(b) for b in mval) + '}'
util.get_debugger().HandleCommand( exec_convert_errors(
f'expr ((unsigned char[{size}])${mname}) = {arr};') f'expr ((unsigned char[{size}])${mname}) = {arr};')
@@ -54,7 +54,6 @@
<attribute schema="VOID" /> <attribute schema="VOID" />
</schema> </schema>
<schema name="WatchpointContainer" canonical="yes" elementResync="NEVER" attributeResync="NEVER"> <schema name="WatchpointContainer" canonical="yes" elementResync="NEVER" attributeResync="NEVER">
<interface name="WatchpointSpecContainer" />
<element schema="WatchpointSpec" /> <element schema="WatchpointSpec" />
<attribute name="_supported_breakpoint_kinds" schema="SET_BREAKPOINT_KIND" required="yes" hidden="yes" /> <attribute name="_supported_breakpoint_kinds" schema="SET_BREAKPOINT_KIND" required="yes" hidden="yes" />
<attribute name="_value" schema="ANY" hidden="yes" /> <attribute name="_value" schema="ANY" hidden="yes" />
@@ -107,7 +106,8 @@
<attribute name="_kind" schema="STRING" fixed="yes" hidden="yes" /> <attribute name="_kind" schema="STRING" fixed="yes" hidden="yes" />
<attribute name="_order" schema="INT" hidden="yes" /> <attribute name="_order" schema="INT" hidden="yes" />
<attribute name="_modified" schema="BOOL" hidden="yes" /> <attribute name="_modified" schema="BOOL" hidden="yes" />
<attribute name="_enabled" schema="BOOL" required="yes" hidden="yes" /> <attribute name="Enabled" schema="BOOL" required="yes" />
<attribute-alias from="_enabled" to="Enabled" />
<attribute name="Commands" schema="STRING" /> <attribute name="Commands" schema="STRING" />
<attribute name="Condition" schema="STRING" /> <attribute name="Condition" schema="STRING" />
<attribute name="Hit Count" schema="INT" /> <attribute name="Hit Count" schema="INT" />
@@ -131,7 +131,8 @@
<attribute name="_kind" schema="STRING" fixed="yes" hidden="yes" /> <attribute name="_kind" schema="STRING" fixed="yes" hidden="yes" />
<attribute name="_order" schema="INT" hidden="yes" /> <attribute name="_order" schema="INT" hidden="yes" />
<attribute name="_modified" schema="BOOL" hidden="yes" /> <attribute name="_modified" schema="BOOL" hidden="yes" />
<attribute name="_enabled" schema="BOOL" required="yes" hidden="yes" /> <attribute name="Enabled" schema="BOOL" required="yes" />
<attribute-alias from="_enabled" to="Enabled" />
<attribute name="_range" schema="RANGE" hidden="yes" /> <attribute name="_range" schema="RANGE" hidden="yes" />
<attribute name="Condition" schema="STRING" /> <attribute name="Condition" schema="STRING" />
<attribute name="Hit Count" schema="INT" /> <attribute name="Hit Count" schema="INT" />
@@ -241,6 +242,8 @@
<attribute name="_kind" schema="STRING" fixed="yes" hidden="yes" /> <attribute name="_kind" schema="STRING" fixed="yes" hidden="yes" />
<attribute name="_order" schema="INT" hidden="yes" /> <attribute name="_order" schema="INT" hidden="yes" />
<attribute name="_modified" schema="BOOL" hidden="yes" /> <attribute name="_modified" schema="BOOL" hidden="yes" />
<attribute name="Enabled" schema="BOOL" required="yes" />
<attribute-alias from="_enabled" to="Enabled" />
<attribute schema="VOID" /> <attribute schema="VOID" />
</schema> </schema>
<schema name="BreakpointLocationContainer" canonical="yes" elementResync="NEVER" attributeResync="NEVER"> <schema name="BreakpointLocationContainer" canonical="yes" elementResync="NEVER" attributeResync="NEVER">
@@ -21,7 +21,7 @@ import sys
import lldb import lldb
LldbVersion = namedtuple('LldbVersion', ['full', 'major', 'minor']) LldbVersion = namedtuple('LldbVersion', ['display', 'full', 'major', 'minor'])
def _compute_lldb_ver(): def _compute_lldb_ver():
@@ -32,7 +32,7 @@ def _compute_lldb_ver():
else: else:
full = top.split('-')[1] # "lldb-x.y.z" full = top.split('-')[1] # "lldb-x.y.z"
major, minor = full.split('.')[:2] major, minor = full.split('.')[:2]
return LldbVersion(full, int(major), int(minor)) return LldbVersion(top, full, int(major), int(minor))
LLDB_VERSION = _compute_lldb_ver() LLDB_VERSION = _compute_lldb_ver()
@@ -150,8 +150,11 @@ class RegionInfoReader(object):
def full_mem(self): def full_mem(self):
# TODO: This may not work for Harvard architectures # TODO: This may not work for Harvard architectures
sizeptr = int(parse_and_eval('sizeof(void*)')) * 8 try:
return Region(0, 1 << sizeptr, 0, None, 'full memory') sizeptr = int(parse_and_eval('sizeof(void*)')) * 8
return Region(0, 1 << sizeptr, 0, None, 'full memory')
except ValueError:
return Region(0, 1 << 64, 0, None, 'full memory')
def _choose_region_info_reader(): def _choose_region_info_reader():
@@ -35,6 +35,12 @@ public enum MacOSSpecimen implements DebuggerTestSpecimen, DebuggerModelTestUtil
return DummyProc.which("expSpin"); return DummyProc.which("expSpin");
} }
}, },
READ {
@Override
public String getCommandLine() {
return DummyProc.which("expRead");
}
},
FORK_EXIT { FORK_EXIT {
@Override @Override
public String getCommandLine() { public String getCommandLine() {
@@ -19,8 +19,7 @@ import java.util.Objects;
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin; import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin;
import ghidra.app.script.GhidraScript; import ghidra.app.script.GhidraScript;
import ghidra.app.services.TraceRmiService; import ghidra.app.services.TraceRmiService;
import ghidra.debug.api.tracermi.TraceRmiAcceptor; import ghidra.debug.api.tracermi.*;
import ghidra.debug.api.tracermi.TraceRmiConnection;
public class ListenTraceRmiScript extends GhidraScript { public class ListenTraceRmiScript extends GhidraScript {
@@ -42,8 +41,18 @@ public class ListenTraceRmiScript extends GhidraScript {
TraceRmiConnection connection = acceptor.accept(); TraceRmiConnection connection = acceptor.accept();
println("Connection from " + connection.getRemoteAddress()); println("Connection from " + connection.getRemoteAddress());
while (askYesNo("Execute?", "Execute 'echo test'?")) { RemoteMethod execute = connection.getMethods().get("execute");
connection.getMethods().get("execute").invoke(Map.of("cmd", "echo test")); if (execute == null) {
printerr("No execute method!");
}
while (true) {
String cmd = askString("Execute", "command?");
try {
execute.invoke(Map.of("cmd", cmd));
}
catch (TraceRmiError e) {
printerr(e.getMessage());
}
} }
} }
} }
@@ -92,7 +92,11 @@ class Receiver(Thread):
dbg_seq = 0 dbg_seq = 0
while not self._is_shutdown: while not self._is_shutdown:
#print("Receiving message") #print("Receiving message")
reply = recv_delimited(self.client.s, bufs.RootMessage(), dbg_seq) try:
reply = recv_delimited(self.client.s, bufs.RootMessage(), dbg_seq)
except BaseException as e:
self._is_shutdown = True
return
#print(f"Got one: {reply.WhichOneof('msg')}") #print(f"Got one: {reply.WhichOneof('msg')}")
dbg_seq += 1 dbg_seq += 1
try: try:
@@ -333,6 +337,14 @@ class Trace(object):
return self.client._delete_bytes(self.id, snap, range) return self.client._delete_bytes(self.id, snap, range)
def put_registers(self, space, values, snap=None): def put_registers(self, space, values, snap=None):
"""
TODO
values is a dictionary, where each key is a a register name, and the
value is a byte array. No matter the target architecture, the value is
given in big-endian byte order.
"""
if snap is None: if snap is None:
snap = self.snap() snap = self.snap()
return self.client._put_registers(self.id, snap, space, values) return self.client._put_registers(self.id, snap, space, values)
@@ -160,6 +160,9 @@ public class DebuggerModulesPanel extends AbstractObjectsTableBasedPanel<TraceOb
DebuggerObjectActionContext ctx) { DebuggerObjectActionContext ctx) {
Set<TraceModule> result = new HashSet<>(); Set<TraceModule> result = new HashSet<>();
for (TraceObjectValue value : ctx.getObjectValues()) { for (TraceObjectValue value : ctx.getObjectValues()) {
if (!value.isObject()) {
continue;
}
TraceObject child = value.getChild(); TraceObject child = value.getChild();
TraceObjectModule module = child.queryInterface(TraceObjectModule.class); TraceObjectModule module = child.queryInterface(TraceObjectModule.class);
if (module != null) { if (module != null) {
@@ -179,6 +182,9 @@ public class DebuggerModulesPanel extends AbstractObjectsTableBasedPanel<TraceOb
DebuggerObjectActionContext ctx) { DebuggerObjectActionContext ctx) {
Set<TraceSection> result = new HashSet<>(); Set<TraceSection> result = new HashSet<>();
for (TraceObjectValue value : ctx.getObjectValues()) { for (TraceObjectValue value : ctx.getObjectValues()) {
if (!value.isObject()) {
continue;
}
TraceObject child = value.getChild(); TraceObject child = value.getChild();
TraceObjectModule module = child.queryInterface(TraceObjectModule.class); TraceObjectModule module = child.queryInterface(TraceObjectModule.class);
if (module != null) { if (module != null) {
+14 -1
View File
@@ -49,10 +49,11 @@ task testSpecimenWin_x86_64 {
task testSpecimenLinux_x86_64 { task testSpecimenLinux_x86_64 {
dependsOn 'expCloneExecExecutable'//Linux_x86_64Executable' dependsOn 'expCloneExecExecutable'//Linux_x86_64Executable'
dependsOn 'expCloneExitExecutable'//Linux_x86_64Executable' dependsOn 'expCloneExitLinux_x86_64Executable'
//dependsOn 'expCloneSpinExecutable'//Linux_x86_64Executable' //dependsOn 'expCloneSpinExecutable'//Linux_x86_64Executable'
dependsOn 'expForkExecutable'//Linux_x86_64Executable' dependsOn 'expForkExecutable'//Linux_x86_64Executable'
dependsOn 'expPrintLinux_x86_64Executable' dependsOn 'expPrintLinux_x86_64Executable'
dependsOn 'expReadLinux_x86_64Executable'
dependsOn 'expSpinLinux_x86_64Executable' dependsOn 'expSpinLinux_x86_64Executable'
//dependsOn 'expTypesExecutable'//Linux_x86_64Executable' //dependsOn 'expTypesExecutable'//Linux_x86_64Executable'
dependsOn 'expRegistersLinux_x86_64Executable' dependsOn 'expRegistersLinux_x86_64Executable'
@@ -67,6 +68,12 @@ task testSpecimenLinux_x86_64 {
} }
} }
task testSpecimenMac_arm_64 {
dependsOn 'expCloneExitMac_arm_64Executable'
dependsOn 'expPrintMac_arm_64Executable'
dependsOn 'expReadMac_arm_64Executable'
}
// TODO: testSpecimenMac_x86_64 (Intel) // TODO: testSpecimenMac_x86_64 (Intel)
// will likely need to codesign them to grant debugee-entitlement // will likely need to codesign them to grant debugee-entitlement
@@ -91,6 +98,7 @@ model {
expCloneExit(NativeExecutableSpec) { expCloneExit(NativeExecutableSpec) {
targetPlatform "linux_x86_64" targetPlatform "linux_x86_64"
//targetPlatform "linux_x86_32" // TODO: Test on these //targetPlatform "linux_x86_32" // TODO: Test on these
targetPlatform "mac_arm_64"
} }
expCloneSpin(NativeExecutableSpec) { expCloneSpin(NativeExecutableSpec) {
targetPlatform "linux_x86_64" targetPlatform "linux_x86_64"
@@ -105,6 +113,11 @@ model {
//targetPlatform "linux_x86_32" // TODO: Test on these //targetPlatform "linux_x86_32" // TODO: Test on these
targetPlatform "win_x86_64" targetPlatform "win_x86_64"
targetPlatform "win_x86_32" // TODO: Test on these targetPlatform "win_x86_32" // TODO: Test on these
targetPlatform "mac_arm_64"
}
expRead(NativeExecutableSpec) {
targetPlatform "linux_x86_64"
targetPlatform "mac_arm_64"
} }
expSpin(NativeExecutableSpec) { expSpin(NativeExecutableSpec) {
targetPlatform "linux_x86_64" targetPlatform "linux_x86_64"
@@ -15,6 +15,7 @@
*/ */
#include <pthread.h> #include <pthread.h>
#include <stdio.h> #include <stdio.h>
#include <unistd.h>
pthread_t thread; pthread_t thread;
@@ -21,7 +21,7 @@
#define DLLEXPORT __declspec(dllexport) #define DLLEXPORT __declspec(dllexport)
#else #else
#define DLLEXPORT #define DLLEXPORT
#define OutputDebugString(out) printf("%s\n", out) #define OutputDebugString(out) puts(out)
#endif #endif
DLLEXPORT volatile char overwrite[] = "Hello, World!"; DLLEXPORT volatile char overwrite[] = "Hello, World!";
@@ -0,0 +1,21 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <unistd.h>
int main(int argc, char** argv) {
char c;
read(0, &c, sizeof(c));
}
@@ -0,0 +1,45 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty;
import java.io.*;
public class StreamPumper extends Thread {
private final InputStream in;
private final OutputStream out;
public StreamPumper(InputStream in, OutputStream out) {
setDaemon(true);
this.in = in;
this.out = out;
}
@Override
public void run() {
byte[] buf = new byte[1024];
try {
while (true) {
int len = in.read(buf);
if (len <= 0) {
break;
}
out.write(buf, 0, len);
}
}
catch (IOException e) {
}
}
}
@@ -24,9 +24,8 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import ghidra.app.script.AskDialog; import ghidra.app.script.AskDialog;
import ghidra.pty.Pty; import ghidra.pty.*;
import ghidra.pty.PtyChild.Echo; import ghidra.pty.PtyChild.Echo;
import ghidra.pty.PtySession;
import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.util.SystemUtilities; import ghidra.util.SystemUtilities;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
@@ -49,33 +48,6 @@ public class SshPtyTest extends AbstractGhidraHeadedIntegrationTest {
return dialog.getValueAsString(); return dialog.getValueAsString();
} }
public static class StreamPumper extends Thread {
private final InputStream in;
private final OutputStream out;
public StreamPumper(InputStream in, OutputStream out) {
setDaemon(true);
this.in = in;
this.out = out;
}
@Override
public void run() {
byte[] buf = new byte[1024];
try {
while (true) {
int len = in.read(buf);
if (len <= 0) {
break;
}
out.write(buf, 0, len);
}
}
catch (IOException e) {
}
}
}
@Test @Test
public void testSessionBash() throws IOException, InterruptedException { public void testSessionBash() throws IOException, InterruptedException {
try (Pty pty = factory.openpty()) { try (Pty pty = factory.openpty()) {
@@ -20,7 +20,8 @@ import static org.junit.Assert.*;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.nio.file.*; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Function; import java.util.function.Function;
@@ -28,8 +29,11 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.io.output.TeeOutputStream;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hamcrest.Matchers;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin; import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin;
@@ -45,6 +49,7 @@ import ghidra.framework.plugintool.PluginsConfiguration;
import ghidra.framework.plugintool.util.*; import ghidra.framework.plugintool.util.*;
import ghidra.program.model.address.Address; import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRangeImpl; import ghidra.program.model.address.AddressRangeImpl;
import ghidra.pty.*;
import ghidra.trace.model.Lifespan; import ghidra.trace.model.Lifespan;
import ghidra.trace.model.breakpoint.TraceBreakpointKind; import ghidra.trace.model.breakpoint.TraceBreakpointKind;
import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet; import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
@@ -54,56 +59,70 @@ import ghidra.util.Msg;
import ghidra.util.NumericUtilities; import ghidra.util.NumericUtilities;
public abstract class AbstractLldbTraceRmiTest extends AbstractGhidraHeadedDebuggerTest { public abstract class AbstractLldbTraceRmiTest extends AbstractGhidraHeadedDebuggerTest {
record PlatDep(String name, String endian, String lang, String cSpec, String callMne,
String intReg, String floatReg) {
static final PlatDep ARM64 =
new PlatDep("arm64", "little", "AARCH64:LE:64:v8A", "default", "bl", "x0", "s0");
static final PlatDep X8664 = // Note AT&T callq
new PlatDep("x86_64", "little", "x86:LE:64:default", "gcc", "callq", "rax", "st0");
}
public static final PlatDep PLAT = computePlat();
static PlatDep computePlat() {
return switch (System.getProperty("os.arch")) {
case "aarch64" -> PlatDep.ARM64;
case "x86" -> PlatDep.X8664;
case "amd64" -> PlatDep.X8664;
default -> throw new AssertionError(
"Unrecognized arch: " + System.getProperty("os.arch"));
};
}
static String getSpecimenClone() {
return DummyProc.which("expCloneExit");
}
static String getSpecimenPrint() {
return DummyProc.which("expPrint");
}
static String getSpecimenRead() {
return DummyProc.which("expRead");
}
/** /**
* Some features have to be disabled to avoid permissions issues in the test container. Namely, * Some features have to be disabled to avoid permissions issues in the test container. Namely,
* don't try to disable ASLR. * don't try to disable ASLR.
*
* Color codes mess up the address parsing.
*/ */
public static final String PREAMBLE = """ public static final String PREAMBLE = """
script import ghidralldb script import ghidralldb
settings set use-color false
settings set target.disable-aslr false settings set target.disable-aslr false
"""; """;
// Connecting should be the first thing the script does, so use a tight timeout. // Connecting should be the first thing the script does, so use a tight timeout.
protected static final int CONNECT_TIMEOUT_MS = 3000; protected static final int CONNECT_TIMEOUT_MS = 3000;
protected static final int TIMEOUT_SECONDS = 300; protected static final int TIMEOUT_SECONDS = 300;
protected static final int QUIT_TIMEOUT_MS = 1000; protected static final int QUIT_TIMEOUT_MS = 1000;
public static final String INSTRUMENT_STOPPED =
"""
ghidra_trace_txopen "Fake" 'ghidra_trace_create_obj Processes[1]'
define do-set-stopped
ghidra_trace_set_value Processes[1] _state '"STOPPED"'
end
define set-stopped
ghidra_trace_txopen Stopped do-set-stopped
end
#lldb.debugger.HandleCommand('target stop-hook add -P ghidralldb.hooks.StopHook')
#python lldb.events.stop.connect(lambda e: lldb.execute("set-stopped"))""";
public static final String INSTRUMENT_RUNNING =
"""
ghidra_trace_txopen "Fake" 'ghidra_trace_create_obj Processes[1]'
define do-set-running
ghidra_trace_set_value Processes[1] _state '"RUNNING"'
end
define set-running
ghidra_trace_txopen Running do-set-running
end
#lldb.debugger.HandleCommand('target stop-hook add -P ghidralldb.hooks.StopHook')
#python lldb.events.cont.connect(lambda e: lldb.execute("set-running"))""";
protected TraceRmiService traceRmi; protected TraceRmiService traceRmi;
private Path lldbPath; private Path lldbPath;
private Path outFile;
private Path errFile;
// @BeforeClass // @BeforeClass
public static void setupPython() throws Throwable { public static void setupPython() throws Throwable {
new ProcessBuilder("gradle", "Debugger-agent-lldb:assemblePyPackage") new ProcessBuilder("gradle",
.directory(TestApplicationUtils.getInstallationDirectory()) "Debugger-rmi-trace:assemblePyPackage",
.inheritIO() "Debugger-agent-lldb:assemblePyPackage")
.start() .directory(TestApplicationUtils.getInstallationDirectory())
.waitFor(); .inheritIO()
.start()
.waitFor();
} }
protected void setPythonPath(ProcessBuilder pb) throws IOException { protected void setPythonPath(Map<String, String> env) throws IOException {
String sep = String sep =
OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS ? ";" : ":"; OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS ? ";" : ":";
String rmiPyPkg = Application.getModuleSubDirectory("Debugger-rmi-trace", String rmiPyPkg = Application.getModuleSubDirectory("Debugger-rmi-trace",
@@ -111,7 +130,7 @@ public abstract class AbstractLldbTraceRmiTest extends AbstractGhidraHeadedDebug
String gdbPyPkg = Application.getModuleSubDirectory("Debugger-agent-lldb", String gdbPyPkg = Application.getModuleSubDirectory("Debugger-agent-lldb",
"build/pypkg/src").getAbsolutePath(); "build/pypkg/src").getAbsolutePath();
String add = rmiPyPkg + sep + gdbPyPkg; String add = rmiPyPkg + sep + gdbPyPkg;
pb.environment().compute("PYTHONPATH", (k, v) -> v == null ? add : (v + sep + add)); env.compute("PYTHONPATH", (k, v) -> v == null ? add : (v + sep + add));
} }
@Before @Before
@@ -124,8 +143,6 @@ public abstract class AbstractLldbTraceRmiTest extends AbstractGhidraHeadedDebug
catch (RuntimeException e) { catch (RuntimeException e) {
lldbPath = Paths.get(DummyProc.which("lldb")); lldbPath = Paths.get(DummyProc.which("lldb"));
} }
outFile = Files.createTempFile("lldbout", null);
errFile = Files.createTempFile("lldberr", null);
} }
protected void addAllDebuggerPlugins() throws PluginException { protected void addAllDebuggerPlugins() throws PluginException {
@@ -156,71 +173,70 @@ public abstract class AbstractLldbTraceRmiTest extends AbstractGhidraHeadedDebug
throw new AssertionError("Unhandled address type " + address); throw new AssertionError("Unhandled address type " + address);
} }
protected record LldbResult(boolean timedOut, int exitCode, String stdout, String stderr) { protected record LldbResult(boolean timedOut, int exitCode, String out) {
protected String handle() { protected String handle() {
if (!"".equals(stderr) || (0 != exitCode && 143 != exitCode)) { if (0 != exitCode && 143 != exitCode) {
throw new LldbError(exitCode, stdout, stderr); throw new LldbError(exitCode, out);
} }
return stdout; return out;
} }
} }
protected record ExecInLldb(Process lldb, CompletableFuture<LldbResult> future) { protected record ExecInLldb(Pty pty, PtySession lldb, CompletableFuture<LldbResult> future,
} Thread pumper) {}
@SuppressWarnings("resource") // Do not close stdin @SuppressWarnings("resource") // Do not close stdin
protected ExecInLldb execInLldb(String script) throws IOException { protected ExecInLldb execInLldb(String script) throws IOException {
ProcessBuilder pb = new ProcessBuilder(lldbPath.toString()); Pty pty = PtyFactory.local().openpty();
setPythonPath(pb); Map<String, String> env = new HashMap<>(System.getenv());
setPythonPath(env);
env.put("TERM", "xterm-256color");
ByteArrayOutputStream capture = new ByteArrayOutputStream();
OutputStream tee = new TeeOutputStream(System.out, capture);
Thread pumper = new StreamPumper(pty.getParent().getInputStream(), tee);
pumper.start();
PtySession lldbSession = pty.getChild().session(new String[] { lldbPath.toString() }, env);
// If commands come from file, LLDB will quit after EOF. OutputStream stdin = pty.getParent().getOutputStream();
Msg.info(this, "outFile: " + outFile);
Msg.info(this, "errFile: " + errFile);
pb.redirectInput(ProcessBuilder.Redirect.PIPE);
pb.redirectOutput(outFile.toFile());
pb.redirectError(errFile.toFile());
Process lldbProc = pb.start();
OutputStream stdin = lldbProc.getOutputStream();
stdin.write(script.getBytes()); stdin.write(script.getBytes());
stdin.flush(); stdin.flush();
return new ExecInLldb(lldbProc, CompletableFuture.supplyAsync(() -> { return new ExecInLldb(pty, lldbSession, CompletableFuture.supplyAsync(() -> {
try { try {
if (!lldbProc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { int exitVal = lldbSession.waitExited(TIMEOUT_SECONDS, TimeUnit.SECONDS);
Msg.error(this, "Timed out waiting for LLDB"); Msg.info(this, "LLDB exited with code " + exitVal);
lldbProc.destroyForcibly(); return new LldbResult(false, exitVal, capture.toString());
lldbProc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS); }
return new LldbResult(true, -1, Files.readString(outFile), catch (TimeoutException e) {
Files.readString(errFile)); return new LldbResult(true, -1, capture.toString());
}
Msg.info(this, "LLDB exited with code " + lldbProc.exitValue());
return new LldbResult(false, lldbProc.exitValue(), Files.readString(outFile),
Files.readString(errFile));
} }
catch (Exception e) { catch (Exception e) {
return ExceptionUtils.rethrow(e); return ExceptionUtils.rethrow(e);
} }
finally { finally {
lldbProc.destroyForcibly(); try {
pty.close();
}
catch (IOException e) {
Msg.warn(this, "Couldn't close pty: " + e);
}
lldbSession.destroyForcibly();
pumper.interrupt();
} }
})); }), pumper);
} }
public static class LldbError extends RuntimeException { public static class LldbError extends RuntimeException {
public final int exitCode; public final int exitCode;
public final String stdout; public final String out;
public final String stderr;
public LldbError(int exitCode, String stdout, String stderr) { public LldbError(int exitCode, String out) {
super(""" super("""
exitCode=%d: exitCode=%d:
----stdout---- ----out----
%s %s
----stderr---- """.formatted(exitCode, out));
%s
""".formatted(exitCode, stdout, stderr));
this.exitCode = exitCode; this.exitCode = exitCode;
this.stdout = stdout; this.out = out;
this.stderr = stderr;
} }
} }
@@ -250,17 +266,32 @@ public abstract class AbstractLldbTraceRmiTest extends AbstractGhidraHeadedDebug
return (String) execute.invoke(Map.of("cmd", cmd, "to_string", true)); return (String) execute.invoke(Map.of("cmd", cmd, "to_string", true));
} }
public Object evaluate(String expr) {
RemoteMethod evaluate = getMethod("evaluate");
return evaluate.invoke(Map.of("expr", expr));
}
public Object pyeval(String expr) {
RemoteMethod pyeval = getMethod("pyeval");
return pyeval.invoke(Map.of("expr", expr));
}
@Override @Override
public void close() throws Exception { public void close() throws Exception {
Msg.info(this, "Cleaning up lldb"); Msg.info(this, "Cleaning up lldb");
exec.lldb().destroy(); execute("settings set auto-confirm true");
exec.pty.getParent().getOutputStream().write("""
quit
""".getBytes());
try { try {
LldbResult r = exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); LldbResult r = exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
r.handle(); r.handle();
waitForPass(() -> assertTrue(connection.isClosed())); waitForPass(() -> assertTrue(connection.isClosed()));
} }
finally { finally {
exec.pty.close();
exec.lldb.destroyForcibly(); exec.lldb.destroyForcibly();
exec.pumper.interrupt();
} }
} }
} }
@@ -276,8 +307,10 @@ public abstract class AbstractLldbTraceRmiTest extends AbstractGhidraHeadedDebug
return new LldbAndConnection(exec, connection); return new LldbAndConnection(exec, connection);
} }
catch (SocketTimeoutException e) { catch (SocketTimeoutException e) {
exec.pty.close();
exec.lldb.destroyForcibly(); exec.lldb.destroyForcibly();
exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle(); exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle();
exec.pumper.interrupt();
throw e; throw e;
} }
} }
@@ -285,7 +318,7 @@ public abstract class AbstractLldbTraceRmiTest extends AbstractGhidraHeadedDebug
protected LldbAndConnection startAndConnectLldb() throws Exception { protected LldbAndConnection startAndConnectLldb() throws Exception {
return startAndConnectLldb(addr -> """ return startAndConnectLldb(addr -> """
%s %s
ghidra_trace_connect %s ghidra trace connect %s
""".formatted(PREAMBLE, addr)); """.formatted(PREAMBLE, addr));
} }
@@ -294,36 +327,56 @@ public abstract class AbstractLldbTraceRmiTest extends AbstractGhidraHeadedDebug
throws Exception { throws Exception {
LldbAndConnection conn = startAndConnectLldb(scriptSupplier); LldbAndConnection conn = startAndConnectLldb(scriptSupplier);
LldbResult r = conn.exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); LldbResult r = conn.exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
conn.exec.pty.close();
conn.exec.pumper.interrupt();
String stdout = r.handle(); String stdout = r.handle();
waitForPass(() -> assertTrue(conn.connection.isClosed())); waitForPass(() -> assertTrue(conn.connection.isClosed()));
return stdout; return stdout;
} }
protected void waitStopped() { protected void waitState(String state) {
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]", Lifespan.at(0))); TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]", Lifespan.at(0)));
waitForPass(() -> assertEquals("STOPPED", tb.objValue(proc, 0, "_state"))); for (int i = 0; i < 5; i++) {
try {
waitForPass(() -> {
Long snap = tb.trace.getTimeManager().getMaxSnap();
assertEquals(state, tb.objValue(proc, snap != null ? snap : 0, "_state"));
});
break;
}
catch (AssertionError e) {
if (i == 4) {
throw e;
}
}
}
waitTxDone(); waitTxDone();
} }
protected void waitRunning() { protected void waitStopped(LldbAndConnection conn) {
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]", Lifespan.at(0))); waitForPass(() -> assertEquals(Boolean.TRUE,
waitForPass(() -> assertEquals("RUNNING", tb.objValue(proc, 0, "_state"))); conn.pyeval("util.get_debugger().GetTargetAtIndex(0).GetProcess().is_stopped")));
waitTxDone(); // waitState("STOPPED");
}
protected void waitRunning(LldbAndConnection conn) {
waitForPass(() -> assertEquals(Boolean.TRUE,
conn.pyeval("util.get_debugger().GetTargetAtIndex(0).GetProcess().is_running")));
// waitState("RUNNING");
} }
protected String extractOutSection(String out, String head) { protected String extractOutSection(String out, String head) {
String[] split = out.split("\n"); String[] split = out.replace("\r", "").split("\n");
String xout = ""; String xout = "";
for (String s : split) { for (String s : split) {
if (!s.startsWith("(lldb)") && !s.equals("")) { if (!s.startsWith("(lldb)") && !s.contains("script print(") && !s.equals("")) {
xout += s + "\n"; xout += s + "\n";
} }
} }
return xout.split(head)[1].split("---")[0].replace("(lldb)", "").trim(); return xout.split(head)[1].split("---")[0].replace("(lldb)", "").trim();
} }
record MemDump(long address, byte[] data) { record MemDump(long address, byte[] data) {}
}
protected MemDump parseHexDump(String dump) throws IOException { protected MemDump parseHexDump(String dump) throws IOException {
// First, get the address. Assume contiguous, so only need top line. // First, get the address. Assume contiguous, so only need top line.
@@ -348,13 +401,6 @@ public abstract class AbstractLldbTraceRmiTest extends AbstractGhidraHeadedDebug
return new MemDump(address, buf.toByteArray()); return new MemDump(address, buf.toByteArray());
} }
record RegDump() {
}
protected RegDump parseRegDump(String dump) {
return new RegDump();
}
protected ManagedDomainObject openDomainObject(String path) throws Exception { protected ManagedDomainObject openDomainObject(String path) throws Exception {
DomainFile df = env.getProject().getProjectData().getFile(path); DomainFile df = env.getProject().getProjectData().getFile(path);
assertNotNull(df); assertNotNull(df);
@@ -376,6 +422,14 @@ public abstract class AbstractLldbTraceRmiTest extends AbstractGhidraHeadedDebug
} }
} }
protected void assertLocalOs(String actual) {
assertThat(actual, Matchers.startsWith(switch (OperatingSystem.CURRENT_OPERATING_SYSTEM) {
case LINUX -> "linux";
case MAC_OS_X -> "macos";
default -> throw new AssertionError("What OS?");
}));
}
protected void assertBreakLoc(TraceObjectValue locVal, String key, Address addr, int len, protected void assertBreakLoc(TraceObjectValue locVal, String key, Address addr, int len,
Set<TraceBreakpointKind> kinds, String expression) throws Exception { Set<TraceBreakpointKind> kinds, String expression) throws Exception {
assertEquals(key, locVal.getEntryKey()); assertEquals(key, locVal.getEntryKey());
@@ -15,7 +15,8 @@
*/ */
package agent.lldb.rmi; package agent.lldb.rmi;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@@ -41,7 +42,7 @@ import ghidra.trace.model.time.TraceSnapshot;
@Category(NightlyCategory.class) // this may actually be an @PortSensitive test @Category(NightlyCategory.class) // this may actually be an @PortSensitive test
public class LldbHooksTest extends AbstractLldbTraceRmiTest { public class LldbHooksTest extends AbstractLldbTraceRmiTest {
private static final long RUN_TIMEOUT_MS = 20000; private static final long RUN_TIMEOUT_MS = 5000;
private static final long RETRY_MS = 500; private static final long RETRY_MS = 500;
record LldbAndTrace(LldbAndConnection conn, ManagedDomainObject mdo) implements AutoCloseable { record LldbAndTrace(LldbAndConnection conn, ManagedDomainObject mdo) implements AutoCloseable {
@@ -64,10 +65,7 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
protected LldbAndTrace startAndSyncLldb() throws Exception { protected LldbAndTrace startAndSyncLldb() throws Exception {
LldbAndConnection conn = startAndConnectLldb(); LldbAndConnection conn = startAndConnectLldb();
try { try {
// TODO: Why does using 'set arch' cause a hang at quit? conn.execute("ghidra trace start");
conn.execute(
"ghidralldb.util.set_convenience_variable('ghidra-language', 'x86:LE:64:default')");
conn.execute("ghidra_trace_start");
ManagedDomainObject mdo = waitDomainObject("/New Traces/lldb/noname"); ManagedDomainObject mdo = waitDomainObject("/New Traces/lldb/noname");
tb = new ToyDBTraceBuilder((Trace) mdo.get()); tb = new ToyDBTraceBuilder((Trace) mdo.get());
return new LldbAndTrace(conn, mdo); return new LldbAndTrace(conn, mdo);
@@ -102,7 +100,7 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
RUN_TIMEOUT_MS, RETRY_MS); RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("continue"); conn.execute("continue");
waitStopped(); waitStopped(conn.conn);
txPut(conn, "threads"); txPut(conn, "threads");
waitForPass(() -> assertEquals(2, waitForPass(() -> assertEquals(2,
tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()), tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()),
@@ -131,7 +129,7 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
RUN_TIMEOUT_MS, RETRY_MS); RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("continue"); conn.execute("continue");
waitStopped(); waitStopped(conn.conn);
waitForPass(() -> { waitForPass(() -> {
TraceObject inf = tb.objAny("Processes[]"); TraceObject inf = tb.objAny("Processes[]");
assertNotNull(inf); assertNotNull(inf);
@@ -207,11 +205,11 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
try (LldbAndTrace conn = startAndSyncLldb()) { try (LldbAndTrace conn = startAndSyncLldb()) {
traceManager.openTrace(tb.trace); traceManager.openTrace(tb.trace);
start(conn, "bash"); start(conn, getSpecimenPrint());
conn.execute("breakpoint set -n read"); conn.execute("breakpoint set -n puts");
conn.execute("cont"); conn.execute("cont");
waitStopped(); waitStopped(conn.conn);
waitForPass(() -> assertThat( waitForPass(() -> assertThat(
tb.objValues(lastSnap(conn), "Processes[].Threads[].Stack[]").size(), tb.objValues(lastSnap(conn), "Processes[].Threads[].Stack[]").size(),
greaterThan(2)), greaterThan(2)),
@@ -224,6 +222,8 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
conn.execute("frame select 0"); conn.execute("frame select 0");
waitForPass(() -> assertEquals("0", frameIndex(traceManager.getCurrentObject())), waitForPass(() -> assertEquals("0", frameIndex(traceManager.getCurrentObject())),
RUN_TIMEOUT_MS, RETRY_MS); RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("kill");
} }
} }
@@ -234,16 +234,16 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
// FWIW, I've already seen this getting exercised in other tests. // FWIW, I've already seen this getting exercised in other tests.
} }
@Test //@Test // LLDB does not provide the necessary events
public void testOnMemoryChanged() throws Exception { public void testOnMemoryChanged() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) { try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash"); start(conn, getSpecimenPrint());
long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]); long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]);
conn.execute("expr *((char*)(void(*)())main) = 0x7f"); conn.execute("expr *((char*)(void(*)())main) = 0x7f");
conn.execute("ghidra_trace_txstart 'Tx'"); //conn.execute("ghidra trace tx-start 'Tx'");
conn.execute("ghidra_trace_putmem `(void(*)())main` 10"); //conn.execute("ghidra trace putmem '(void(*)())main' 10");
conn.execute("ghidra_trace_txcommit"); //conn.execute("ghidra trace tx-commit");
waitForPass(() -> { waitForPass(() -> {
ByteBuffer buf = ByteBuffer.allocate(10); ByteBuffer buf = ByteBuffer.allocate(10);
@@ -253,15 +253,15 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
} }
} }
@Test //@Test // LLDB does not provide the necessary events
public void testOnRegisterChanged() throws Exception { public void testOnRegisterChanged() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) { try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash"); start(conn, getSpecimenPrint());
conn.execute("expr $rax = 0x1234"); conn.execute("expr $%s = 0x1234".formatted(PLAT.intReg()));
conn.execute("ghidra_trace_txstart 'Tx'"); //conn.execute("ghidra trace tx-start 'Tx'");
conn.execute("ghidra_trace_putreg"); //conn.execute("ghidra trace putreg");
conn.execute("ghidra_trace_txcommit"); //conn.execute("ghidra trace tx-commit");
String path = "Processes[].Threads[].Stack[].Registers"; String path = "Processes[].Threads[].Stack[].Registers";
TraceObject registers = Objects.requireNonNull(tb.objAny(path, Lifespan.at(0))); TraceObject registers = Objects.requireNonNull(tb.objAny(path, Lifespan.at(0)));
@@ -269,29 +269,33 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
.getAddressSpace(registers.getCanonicalPath().toString()); .getAddressSpace(registers.getCanonicalPath().toString());
TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(space, false); TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(space, false);
waitForPass(() -> assertEquals("1234", waitForPass(() -> assertEquals("1234",
regs.getValue(lastSnap(conn), tb.reg("RAX")).getUnsignedValue().toString(16))); regs.getValue(lastSnap(conn), tb.reg(PLAT.intReg()))
.getUnsignedValue()
.toString(16)));
} }
} }
@Test @Test
public void testOnCont() throws Exception { public void testOnCont() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) { try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash"); start(conn, getSpecimenRead());
conn.execute("cont"); conn.execute("cont");
waitRunning(); waitRunning(conn.conn);
TraceObject proc = waitForValue(() -> tb.objAny("Processes[]")); TraceObject proc = waitForValue(() -> tb.objAny("Processes[]"));
waitForPass(() -> { waitForPass(() -> {
assertEquals("RUNNING", tb.objValue(proc, lastSnap(conn), "_state")); assertEquals("RUNNING", tb.objValue(proc, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS); }, RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("process interrupt");
} }
} }
@Test @Test
public void testOnStop() throws Exception { public void testOnStop() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) { try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash"); start(conn, getSpecimenPrint());
TraceObject inf = waitForValue(() -> tb.objAny("Processes[]")); TraceObject inf = waitForValue(() -> tb.objAny("Processes[]"));
waitForPass(() -> { waitForPass(() -> {
@@ -303,25 +307,22 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
@Test @Test
public void testOnExited() throws Exception { public void testOnExited() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) { try (LldbAndTrace conn = startAndSyncLldb()) {
conn.execute("file bash"); start(conn, getSpecimenPrint());
conn.execute("ghidra_trace_sync_enable");
conn.execute("process launch --stop-at-entry -- -c 'exit 1'");
txPut(conn, "processes");
conn.execute("cont"); conn.execute("cont");
waitRunning(); waitRunning(conn.conn);
waitForPass(() -> { waitForPass(() -> {
TraceSnapshot snapshot = TraceSnapshot snapshot =
tb.trace.getTimeManager().getSnapshot(lastSnap(conn), false); tb.trace.getTimeManager().getSnapshot(lastSnap(conn), false);
assertNotNull(snapshot); assertNotNull(snapshot);
assertEquals("Exited with code 1", snapshot.getDescription()); assertEquals("Exited with code 72", snapshot.getDescription());
TraceObject proc = tb.objAny("Processes[]"); TraceObject proc = tb.objAny("Processes[]");
assertNotNull(proc); assertNotNull(proc);
Object val = tb.objValue(proc, lastSnap(conn), "_exit_code"); Object val = tb.objValue(proc, lastSnap(conn), "_exit_code");
assertThat(val, instanceOf(Number.class)); assertThat(val, instanceOf(Number.class));
assertEquals(1, ((Number) val).longValue()); assertEquals(72, ((Number) val).longValue());
}, RUN_TIMEOUT_MS, RETRY_MS); }, RUN_TIMEOUT_MS, RETRY_MS);
} }
} }
@@ -329,7 +330,7 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
@Test @Test
public void testOnBreakpointCreated() throws Exception { public void testOnBreakpointCreated() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) { try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash"); start(conn, getSpecimenPrint());
assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size()); assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size());
conn.execute("breakpoint set -n main"); conn.execute("breakpoint set -n main");
@@ -346,9 +347,10 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
@Test @Test
public void testOnBreakpointModified() throws Exception { public void testOnBreakpointModified() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) { try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash"); start(conn, getSpecimenPrint());
assertEquals(0, tb.objValues(lastSnap(conn), "Breakpoints[]").size()); assertEquals(0, tb.objValues(lastSnap(conn), "Breakpoints[]").size());
//conn.execute("script lldb.debugger.SetAsync(False)");
conn.execute("breakpoint set -n main"); conn.execute("breakpoint set -n main");
conn.execute("stepi"); conn.execute("stepi");
TraceObject brk = waitForPass(() -> { TraceObject brk = waitForPass(() -> {
@@ -357,6 +359,8 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
return (TraceObject) brks.get(0); return (TraceObject) brks.get(0);
}); });
assertEquals(null, tb.objValue(brk, lastSnap(conn), "Condition")); assertEquals(null, tb.objValue(brk, lastSnap(conn), "Condition"));
waitStopped(conn.conn);
conn.execute("breakpoint modify -c 'x>3'"); conn.execute("breakpoint modify -c 'x>3'");
conn.execute("stepi"); conn.execute("stepi");
// NB: Testing "Commands" requires multi-line input - not clear how to do this // NB: Testing "Commands" requires multi-line input - not clear how to do this
@@ -372,7 +376,7 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
@Test @Test
public void testOnBreakpointDeleted() throws Exception { public void testOnBreakpointDeleted() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) { try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash"); start(conn, getSpecimenPrint());
assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size()); assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size());
conn.execute("breakpoint set -n main"); conn.execute("breakpoint set -n main");
@@ -384,6 +388,7 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
return (TraceObject) brks.get(0); return (TraceObject) brks.get(0);
}); });
waitStopped(conn.conn);
conn.execute("breakpoint delete %s".formatted(brk.getCanonicalPath().index())); conn.execute("breakpoint delete %s".formatted(brk.getCanonicalPath().index()));
conn.execute("stepi"); conn.execute("stepi");
@@ -395,15 +400,15 @@ public class LldbHooksTest extends AbstractLldbTraceRmiTest {
private void start(LldbAndTrace conn, String obj) { private void start(LldbAndTrace conn, String obj) {
conn.execute("file " + obj); conn.execute("file " + obj);
conn.execute("ghidra_trace_sync_enable"); conn.execute("ghidra trace sync-enable");
conn.execute("process launch --stop-at-entry"); conn.execute("process launch --stop-at-entry");
txPut(conn, "processes"); txPut(conn, "processes");
} }
private void txPut(LldbAndTrace conn, String obj) { private void txPut(LldbAndTrace conn, String obj) {
conn.execute("ghidra_trace_txstart 'Tx" + obj + "'"); conn.execute("ghidra trace tx-start 'Tx" + obj + "'");
conn.execute("ghidra_trace_put_" + obj); conn.execute("ghidra trace put-" + obj);
conn.execute("ghidra_trace_txcommit"); conn.execute("ghidra trace tx-commit");
} }
} }