GP-5271: Fix break/watchpoints with lldb. Many framework changes.

This commit is contained in:
Dan 2025-01-17 07:58:01 -05:00
parent 3bfcb8695f
commit 2a41c8fe6b
16 changed files with 335 additions and 260 deletions

View file

@ -42,20 +42,16 @@ DEFAULT_REGISTER_BANK = "General Purpose Registers"
AVAILABLES_PATH = 'Available'
AVAILABLE_KEY_PATTERN = '[{pid}]'
AVAILABLE_PATTERN = AVAILABLES_PATH + AVAILABLE_KEY_PATTERN
BREAKPOINTS_PATH = 'Breakpoints'
BREAKPOINT_KEY_PATTERN = '[{breaknum}]'
BREAKPOINT_PATTERN = BREAKPOINTS_PATH + BREAKPOINT_KEY_PATTERN
WATCHPOINTS_PATH = 'Watchpoints'
WATCHPOINT_KEY_PATTERN = '[{watchnum}]'
WATCHPOINT_PATTERN = WATCHPOINTS_PATH + WATCHPOINT_KEY_PATTERN
BREAK_LOC_KEY_PATTERN = '[{locnum}]'
PROCESSES_PATH = 'Processes'
PROCESS_KEY_PATTERN = '[{procnum}]'
PROCESS_PATTERN = PROCESSES_PATH + PROCESS_KEY_PATTERN
PROC_WATCHES_PATTERN = PROCESS_PATTERN + '.Watchpoints'
PROC_WATCH_KEY_PATTERN = PROC_WATCHES_PATTERN + '[{watchnum}]'
PROC_WATCH_PATTERN = PROC_WATCHES_PATTERN + WATCHPOINT_KEY_PATTERN
PROC_BREAKS_PATTERN = PROCESS_PATTERN + '.Breakpoints'
PROC_BREAK_KEY_PATTERN = '[{breaknum}.{locnum}]'
PROC_BREAK_PATTERN = PROC_BREAKS_PATTERN + BREAKPOINT_KEY_PATTERN
ENV_PATTERN = PROCESS_PATTERN + '.Environment'
THREADS_PATTERN = PROCESS_PATTERN + '.Threads'
THREAD_KEY_PATTERN = '[{tnum}]'
@ -1474,108 +1470,102 @@ def ghidra_trace_put_available(debugger, command, result, internal_dict):
put_available()
def put_single_breakpoint(b, ibobj, proc, ikeys):
def put_single_breakpoint(b, proc):
mapper = STATE.trace.memory_mapper
bpath = BREAKPOINT_PATTERN.format(breaknum=b.GetID())
brkobj = STATE.trace.create_object(bpath)
bpt_path = PROC_BREAK_PATTERN.format(
procnum=proc.GetProcessID(), breaknum=b.GetID())
bpt_obj = STATE.trace.create_object(bpt_path)
if b.IsHardware():
brkobj.set_value('Expression', util.get_description(b))
brkobj.set_value('Kinds', 'HW_EXECUTE')
bpt_obj.set_value('Expression', util.get_description(b))
bpt_obj.set_value('Kinds', 'HW_EXECUTE')
else:
brkobj.set_value('Expression', util.get_description(b))
brkobj.set_value('Kinds', 'SW_EXECUTE')
bpt_obj.set_value('Expression', util.get_description(b))
bpt_obj.set_value('Kinds', 'SW_EXECUTE')
cmdList = lldb.SBStringList()
if b.GetCommandLineCommands(cmdList):
list = []
for i in range(0, cmdList.GetSize()):
list.append(cmdList.GetStringAtIndex(i))
brkobj.set_value('Commands', list)
bpt_obj.set_value('Commands', list)
if b.GetCondition():
brkobj.set_value('Condition', b.GetCondition())
brkobj.set_value('Hit Count', b.GetHitCount())
brkobj.set_value('Ignore Count', b.GetIgnoreCount())
brkobj.set_value('Temporary', b.IsOneShot())
brkobj.set_value('Enabled', b.IsEnabled())
keys = []
bpt_obj.set_value('Condition', b.GetCondition())
bpt_obj.set_value('Hit Count', b.GetHitCount())
bpt_obj.set_value('Ignore Count', b.GetIgnoreCount())
bpt_obj.set_value('Temporary', b.IsOneShot())
bpt_obj.set_value('Enabled', b.IsEnabled())
loc_keys = []
locs = util.BREAKPOINT_LOCATION_INFO_READER.get_locations(b)
hooks.BRK_STATE.update_brkloc_count(b, len(locs))
for i, l in enumerate(locs):
# Retain the key, even if not for this process
k = BREAK_LOC_KEY_PATTERN.format(locnum=i+1)
keys.append(k)
locobj = STATE.trace.create_object(bpath + k)
ik = PROC_BREAK_KEY_PATTERN.format(breaknum=b.GetID(), locnum=i+1)
ikeys.append(ik)
loc_keys.append(k)
loc_obj = STATE.trace.create_object(bpt_path + k)
if b.location is not None: # Implies execution break
base, addr = mapper.map(proc, l.GetLoadAddress())
if base != addr.space:
STATE.trace.create_overlay_space(base, addr.space)
locobj.set_value('Range', addr.extend(1))
locobj.set_value('Enabled', l.IsEnabled())
loc_obj.set_value('Range', addr.extend(1))
loc_obj.set_value('Enabled', l.IsEnabled())
else: # I guess it's a catchpoint
pass
locobj.insert()
ibobj.set_value(ik, locobj)
brkobj.retain_values(keys)
brkobj.insert()
loc_obj.insert()
bpt_obj.retain_values(loc_keys)
bpt_obj.insert()
def put_single_watchpoint(b, ibobj, proc, ikeys):
def put_single_watchpoint(w, proc):
mapper = STATE.trace.memory_mapper
bpath = PROC_WATCH_KEY_PATTERN.format(
procnum=proc.GetProcessID(), watchnum=b.GetID())
brkobj = STATE.trace.create_object(bpath)
desc = util.get_description(b, level=0)
brkobj.set_value('Expression', desc)
brkobj.set_value('Kinds', 'WRITE')
wpt_path = PROC_WATCH_PATTERN.format(
procnum=proc.GetProcessID(), watchnum=w.GetID())
wpt_obj = STATE.trace.create_object(wpt_path)
desc = util.get_description(w, level=0)
wpt_obj.set_value('Expression', desc)
wpt_obj.set_value('Kinds', 'WRITE')
if "type = r" in desc:
brkobj.set_value('Kinds', 'READ')
wpt_obj.set_value('Kinds', 'READ')
if "type = rw" in desc:
brkobj.set_value('Kinds', 'READ,WRITE')
base, addr = mapper.map(proc, b.GetWatchAddress())
wpt_obj.set_value('Kinds', 'READ,WRITE')
base, addr = mapper.map(proc, w.GetWatchAddress())
if base != addr.space:
STATE.trace.create_overlay_space(base, addr.space)
brkobj.set_value('Range', addr.extend(b.GetWatchSize()))
if b.GetCondition():
brkobj.set_value('Condition', b.GetCondition())
brkobj.set_value('Hit Count', b.GetHitCount())
brkobj.set_value('Ignore Count', b.GetIgnoreCount())
brkobj.set_value('Hardware Index', b.GetHardwareIndex())
brkobj.set_value('Watch Address', hex(b.GetWatchAddress()))
brkobj.set_value('Watch Size', b.GetWatchSize())
brkobj.set_value('Enabled', b.IsEnabled())
brkobj.insert()
wpt_obj.set_value('Range', addr.extend(w.GetWatchSize()))
if w.GetCondition():
wpt_obj.set_value('Condition', w.GetCondition())
wpt_obj.set_value('Hit Count', w.GetHitCount())
wpt_obj.set_value('Ignore Count', w.GetIgnoreCount())
wpt_obj.set_value('Hardware Index', w.GetHardwareIndex())
wpt_obj.set_value('Watch Address', hex(w.GetWatchAddress()))
wpt_obj.set_value('Watch Size', w.GetWatchSize())
wpt_obj.set_value('Enabled', w.IsEnabled())
wpt_obj.insert()
def put_breakpoints():
target = util.get_target()
proc = util.get_process()
ibpath = PROC_BREAKS_PATTERN.format(procnum=proc.GetProcessID())
ibobj = STATE.trace.create_object(ibpath)
cont_path = PROC_BREAKS_PATTERN.format(procnum=proc.GetProcessID())
cont_obj = STATE.trace.create_object(cont_path)
keys = []
ikeys = []
for i in range(0, target.GetNumBreakpoints()):
b = target.GetBreakpointAtIndex(i)
keys.append(BREAKPOINT_KEY_PATTERN.format(breaknum=b.GetID()))
put_single_breakpoint(b, ibobj, proc, ikeys)
ibobj.insert()
STATE.trace.proxy_object_path(BREAKPOINTS_PATH).retain_values(keys)
ibobj.retain_values(ikeys)
put_single_breakpoint(b, proc)
cont_obj.insert()
cont_obj.retain_values(keys)
def put_watchpoints():
target = util.get_target()
proc = util.get_process()
ibpath = PROC_WATCHES_PATTERN.format(procnum=proc.GetProcessID())
ibobj = STATE.trace.create_object(ibpath)
cont_path = PROC_WATCHES_PATTERN.format(procnum=proc.GetProcessID())
cont_obj = STATE.trace.create_object(cont_path)
keys = []
ikeys = []
for i in range(0, target.GetNumWatchpoints()):
b = target.GetWatchpointAtIndex(i)
keys.append(WATCHPOINT_KEY_PATTERN.format(watchnum=b.GetID()))
put_single_watchpoint(b, ibobj, proc, ikeys)
ibobj.insert()
STATE.trace.proxy_object_path(WATCHPOINTS_PATH).retain_values(keys)
put_single_watchpoint(b, proc)
cont_obj.insert()
cont_obj.retain_values(keys)
@convert_errors

View file

@ -110,28 +110,7 @@ class ProcessState(object):
procobj.set_value('State', 'TERMINATED')
class BrkState(object):
__slots__ = ('break_loc_counts',)
def __init__(self):
self.break_loc_counts = {}
def update_brkloc_count(self, b, count):
self.break_loc_counts[b.GetID()] = count
def get_brkloc_count(self, b):
return self.break_loc_counts.get(b.GetID(), 0)
def del_brkloc_count(self, b):
if b.GetID() not in self.break_loc_counts:
return 0 # TODO: Print a warning?
count = self.break_loc_counts[b.GetID()]
del self.break_loc_counts[b.GetID()]
return count
HOOK_STATE = HookState()
BRK_STATE = BrkState()
PROC_STATE = {}
@ -240,9 +219,9 @@ def process_event(self, listener, event):
return True
if lldb.SBWatchpoint.EventIsWatchpointEvent(event):
btype = lldb.SBWatchpoint.GetWatchpointEventTypeFromEvent(event)
bpt = lldb.SBWatchpoint.GetWatchpointFromEvent(eventt)
bpt = lldb.SBWatchpoint.GetWatchpointFromEvent(event)
if btype is lldb.eWatchpointEventTypeAdded:
return on_watchpoint_added(bpt)
return on_watchpoint_created(bpt)
if btype is lldb.eWatchpointEventTypeCommandChanged:
return on_watchpoint_modified(bpt)
if btype is lldb.eWatchpointEventTypeConditionChanged:
@ -550,18 +529,6 @@ def on_exited(event):
commands.activate()
def notify_others_breaks(proc):
for num, state in PROC_STATE.items():
if num != proc.GetProcessID():
state.breaks = True
def notify_others_watches(proc):
for num, state in PROC_STATE.items():
if num != proc.GetProcessID():
state.watches = True
def modules_changed():
# Assumption: affects the current process
proc = util.get_process()
@ -580,111 +547,80 @@ def on_free_objfile(event):
def on_breakpoint_created(b):
proc = util.get_process()
notify_others_breaks(proc)
if proc.GetProcessID() not in PROC_STATE:
return
trace = commands.STATE.trace
if trace is None:
return
ibpath = commands.PROC_BREAKS_PATTERN.format(procnum=proc.GetProcessID())
with commands.STATE.client.batch():
with trace.open_tx("Breakpoint {} created".format(b.GetID())):
ibobj = trace.create_object(ibpath)
# Do not use retain_values or it'll remove other locs
commands.put_single_breakpoint(b, ibobj, proc, [])
ibobj.insert()
commands.put_single_breakpoint(b, proc)
def on_breakpoint_modified(b):
proc = util.get_process()
notify_others_breaks(proc)
if proc.GetProcessID() not in PROC_STATE:
return
old_count = BRK_STATE.get_brkloc_count(b)
trace = commands.STATE.trace
if trace is None:
return
ibpath = commands.PROC_BREAKS_PATTERN.format(procnum=proc.GetProcessID())
with commands.STATE.client.batch():
with trace.open_tx("Breakpoint {} modified".format(b.GetID())):
ibobj = trace.create_object(ibpath)
commands.put_single_breakpoint(b, ibobj, proc, [])
new_count = BRK_STATE.get_brkloc_count(b)
# NOTE: Location may not apply to process, but whatever.
for i in range(new_count, old_count):
ikey = commands.PROC_BREAK_KEY_PATTERN.format(
breaknum=b.GetID(), locnum=i+1)
ibobj.set_value(ikey, None)
commands.put_single_breakpoint(b, proc)
def on_breakpoint_deleted(b):
proc = util.get_process()
notify_others_breaks(proc)
if proc.GetProcessID() not in PROC_STATE:
return
old_count = BRK_STATE.del_brkloc_count(b)
trace = commands.STATE.trace
if trace is None:
return
bpath = commands.BREAKPOINT_PATTERN.format(breaknum=b.GetID())
ibobj = trace.proxy_object_path(
commands.PROC_BREAKS_PATTERN.format(procnum=proc.GetProcessID()))
bpt_path = commands.PROC_BREAK_PATTERN.format(
procnum=proc.GetProcessID(), breaknum=b.GetID())
bpt_obj = trace.proxy_object_path(bpt_path)
with commands.STATE.client.batch():
with trace.open_tx("Breakpoint {} deleted".format(b.GetID())):
trace.proxy_object_path(bpath).remove(tree=True)
for i in range(old_count):
ikey = commands.PROC_BREAK_KEY_PATTERN.format(
breaknum=b.GetID(), locnum=i+1)
ibobj.set_value(ikey, None)
bpt_obj.remove(tree=True)
def on_watchpoint_created(b):
proc = util.get_process()
notify_others_watches(proc)
if proc.GetProcessID() not in PROC_STATE:
return
trace = commands.STATE.trace
if trace is None:
return
ibpath = commands.PROC_WATCHES_PATTERN.format(procnum=proc.GetProcessID())
with commands.STATE.client.batch():
with trace.open_tx("Breakpoint {} created".format(b.GetID())):
ibobj = trace.create_object(ibpath)
# Do not use retain_values or it'll remove other locs
commands.put_single_watchpoint(b, ibobj, proc, [])
ibobj.insert()
commands.put_single_watchpoint(b, proc)
def on_watchpoint_modified(b):
proc = util.get_process()
notify_others_watches(proc)
if proc.GetProcessID() not in PROC_STATE:
return
old_count = BRK_STATE.get_brkloc_count(b)
trace = commands.STATE.trace
if trace is None:
return
ibpath = commands.PROC_WATCHES_PATTERN.format(procnum=proc.GetProcessID())
with commands.STATE.client.batch():
with trace.open_tx("Watchpoint {} modified".format(b.GetID())):
ibobj = trace.create_object(ibpath)
commands.put_single_watchpoint(b, ibobj, proc, [])
commands.put_single_watchpoint(b, proc)
def on_watchpoint_deleted(b):
proc = util.get_process()
notify_others_watches(proc)
if proc.GetProcessID() not in PROC_STATE:
return
trace = commands.STATE.trace
if trace is None:
return
bpath = commands.WATCHPOINT_PATTERN.format(watchnum=b.GetID())
ibobj = trace.proxy_object_path(
commands.PROC_WATCHES_PATTERN.format(procnum=proc.GetProcessID()))
wpt_path = commands.PROC_WATCH_PATTERN.format(
procnum=proc.GetProcessID(), watchnum=b.GetID())
wpt_obj = trace.proxy_object_path(wpt_path)
with commands.STATE.client.batch():
with trace.open_tx("Watchpoint {} deleted".format(b.GetID())):
trace.proxy_object_path(bpath).remove(tree=True)
wpt_obj.remove(tree=True)
def install_hooks():

View file

@ -33,13 +33,12 @@ def extre(base, ext):
AVAILABLE_PATTERN = re.compile('Available\[(?P<pid>\\d*)\]')
WATCHPOINT_PATTERN = re.compile('Watchpoints\[(?P<watchnum>\\d*)\]')
BREAKPOINT_PATTERN = re.compile('Breakpoints\[(?P<breaknum>\\d*)\]')
BREAK_LOC_PATTERN = extre(BREAKPOINT_PATTERN, '\[(?P<locnum>\\d*)\]')
PROCESS_PATTERN = re.compile('Processes\[(?P<procnum>\\d*)\]')
PROC_BREAKS_PATTERN = extre(PROCESS_PATTERN, '\.Breakpoints')
PROC_BREAK_PATTERN = extre(PROC_BREAKS_PATTERN, '\[(?P<breaknum>\\d*)\]')
PROC_BREAKLOC_PATTERN = extre(PROC_BREAK_PATTERN, '\[(?P<locnum>\\d*)\]')
PROC_WATCHES_PATTERN = extre(PROCESS_PATTERN, '\.Watchpoints')
PROC_WATCHLOC_PATTERN = extre(PROC_WATCHES_PATTERN, '\[(?P<watchnum>\\d*)\]')
PROC_WATCH_PATTERN = extre(PROC_WATCHES_PATTERN, '\[(?P<watchnum>\\d*)\]')
ENV_PATTERN = extre(PROCESS_PATTERN, '\.Environment')
THREADS_PATTERN = extre(PROCESS_PATTERN, '\.Threads')
THREAD_PATTERN = extre(THREADS_PATTERN, '\[(?P<tnum>\\d*)\]')
@ -183,7 +182,7 @@ def find_bpt_by_pattern(pattern, object, err_msg):
def find_bpt_by_obj(object):
return find_bpt_by_pattern(BREAKPOINT_PATTERN, object, "a BreakpointSpec")
return find_bpt_by_pattern(PROC_BREAK_PATTERN, object, "a BreakpointSpec")
# Oof. no lldb/Python method to get breakpoint by number
@ -206,7 +205,7 @@ def find_wpt_by_pattern(pattern, object, err_msg):
def find_wpt_by_obj(object):
return find_wpt_by_pattern(PROC_WATCHLOC_PATTERN, object, "a WatchpointSpec")
return find_wpt_by_pattern(PROC_WATCH_PATTERN, object, "a WatchpointSpec")
def find_bptlocnum_by_pattern(pattern, object, err_msg):
@ -219,7 +218,7 @@ def find_bptlocnum_by_pattern(pattern, object, err_msg):
def find_bptlocnum_by_obj(object):
return find_bptlocnum_by_pattern(BREAK_LOC_PATTERN, object,
return find_bptlocnum_by_pattern(PROC_BREAKLOC_PATTERN, object,
"a BreakpointLocation")
@ -607,17 +606,20 @@ def break_exception(lang: str):
@REGISTRY.method(action='toggle')
def toggle_watchpoint(breakpoint: sch.Schema('WatchpointSpec'), enabled: bool):
def toggle_watchpoint(watchpoint: sch.Schema('WatchpointSpec'), enabled: bool):
"""Toggle a watchpoint."""
wpt = find_wpt_by_obj(watchpoint)
wpt.enabled = enabled
cmd = 'enable' if enabled else 'disable'
exec_convert_errors(f'watchpoint {cmd} {wpt.GetID()}')
@REGISTRY.method(action='toggle')
def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool):
"""Toggle a breakpoint."""
bpt = find_bpt_by_obj(breakpoint)
bpt.enabled = enabled
cmd = 'enable' if enabled else 'disable'
exec_convert_errors(f'breakpoint {cmd} {bpt.GetID()}')
@REGISTRY.method(action='toggle')

View file

@ -6,8 +6,6 @@
<element schema="VOID" />
<attribute name="Processes" schema="ProcessContainer" required="yes" fixed="yes" />
<attribute name="Available" schema="AvailableContainer" required="yes" fixed="yes" />
<attribute name="Breakpoints" schema="BreakpointContainer" required="yes" fixed="yes" />
<attribute name="Watchpoints" schema="WatchpointContainer" required="yes" fixed="yes" />
<attribute name="_event_thread" schema="OBJECT" hidden="yes" />
<attribute name="_focus" schema="Selectable" required="yes" hidden="yes" />
<attribute name="_display" schema="STRING" hidden="yes" />
@ -62,6 +60,7 @@
</schema>
<schema name="WatchpointSpec" canonical="yes" elementResync="NEVER" attributeResync="NEVER">
<interface name="BreakpointSpec" />
<interface name="BreakpointLocation" />
<interface name="Togglable" />
<attribute name="Expression" schema="STRING" required="yes" hidden="yes" />
<attribute-alias from="_expression" to="Expression" />
@ -93,7 +92,7 @@
<interface name="ExecutionStateful" />
<element schema="VOID" />
<attribute name="Threads" schema="ThreadContainer" required="yes" fixed="yes" />
<attribute name="Breakpoints" schema="BreakpointLocationContainer" required="yes" fixed="yes" />
<attribute name="Breakpoints" schema="BreakpointContainer" required="yes" fixed="yes" />
<attribute name="Watchpoints" schema="WatchpointContainer" required="yes" fixed="yes" />
<attribute name="Exit Code" schema="LONG" />
<attribute-alias from="_exit_code" to="Exit Code" />

View file

@ -60,12 +60,6 @@ import ghidra.util.Msg;
import ghidra.util.task.TaskMonitor;
public class TraceRmiTarget extends AbstractTarget {
private static final String BREAK_HW_EXEC = "breakHwExec";
private static final String BREAK_SW_EXEC = "breakSwExec";
private static final String BREAK_READ = "breakRead";
private static final String BREAK_WRITE = "breakWrite";
private static final String BREAK_ACCESS = "breakAccess";
private final TraceRmiConnection connection;
private final Trace trace;
@ -510,14 +504,15 @@ public class TraceRmiTarget extends AbstractTarget {
}
interface MethodMatcher {
default MatchedMethod match(RemoteMethod method, SchemaContext ctx) {
default MatchedMethod match(RemoteMethod method, TraceObjectSchema rootSchema,
KeyPath path) {
List<ParamSpec> spec = spec();
if (spec.size() != method.parameters().size()) {
return null;
}
Map<String, RemoteParameter> found = new HashMap<>();
for (ParamSpec ps : spec) {
RemoteParameter param = ps.find(method, ctx);
RemoteParameter param = ps.find(method, rootSchema, path);
if (param == null) {
return null;
}
@ -530,10 +525,10 @@ public class TraceRmiTarget extends AbstractTarget {
int score();
static MatchedMethod matchPreferredForm(RemoteMethod method, SchemaContext ctx,
List<? extends MethodMatcher> preferred) {
static MatchedMethod matchPreferredForm(RemoteMethod method, TraceObjectSchema rootSchema,
KeyPath path, List<? extends MethodMatcher> preferred) {
return preferred.stream()
.map(m -> m.match(method, ctx))
.map(m -> m.match(method, rootSchema, path))
.filter(m -> m != null)
.findFirst()
.orElse(null);
@ -549,7 +544,8 @@ public class TraceRmiTarget extends AbstractTarget {
}
protected static boolean typeMatches(RemoteMethod method, RemoteParameter param,
SchemaContext ctx, Class<?> type) {
TraceObjectSchema rootSchema, KeyPath path, Class<?> type) {
SchemaContext ctx = rootSchema.getContext();
TraceObjectSchema sch = ctx.getSchemaOrNull(param.type());
if (sch == null) {
throw new RuntimeException(
@ -561,8 +557,16 @@ public class TraceRmiTarget extends AbstractTarget {
return sch == PrimitiveTraceObjectSchema.OBJECT;
}
else if (TraceObjectInterface.class.isAssignableFrom(type)) {
if (path == null) {
return sch.getInterfaces().contains(type);
}
KeyPath found =
rootSchema.searchForSuitable(type.asSubclass(TraceObjectInterface.class), path);
if (found == null) {
return false;
}
return sch == rootSchema.getSuccessorSchema(path);
}
else {
return sch.getType() == type;
}
@ -571,12 +575,13 @@ public class TraceRmiTarget extends AbstractTarget {
interface ParamSpec {
String name();
RemoteParameter find(RemoteMethod method, SchemaContext ctx);
RemoteParameter find(RemoteMethod method, TraceObjectSchema rootSchema, KeyPath path);
}
record SchemaParamSpec(String name, SchemaName schema) implements ParamSpec {
@Override
public RemoteParameter find(RemoteMethod method, SchemaContext ctx) {
public RemoteParameter find(RemoteMethod method, TraceObjectSchema rootSchema,
KeyPath path) {
List<RemoteParameter> withType = method.parameters()
.values()
.stream()
@ -591,11 +596,12 @@ public class TraceRmiTarget extends AbstractTarget {
record TypeParamSpec(String name, Class<?> type) implements ParamSpec {
@Override
public RemoteParameter find(RemoteMethod method, SchemaContext ctx) {
public RemoteParameter find(RemoteMethod method, TraceObjectSchema rootSchema,
KeyPath path) {
List<RemoteParameter> withType = method.parameters()
.values()
.stream()
.filter(p -> typeMatches(method, p, ctx, type))
.filter(p -> typeMatches(method, p, rootSchema, path, type))
.toList();
if (withType.size() != 1) {
return null;
@ -606,9 +612,10 @@ public class TraceRmiTarget extends AbstractTarget {
record NameParamSpec(String name, Class<?> type) implements ParamSpec {
@Override
public RemoteParameter find(RemoteMethod method, SchemaContext ctx) {
public RemoteParameter find(RemoteMethod method, TraceObjectSchema rootSchema,
KeyPath path) {
RemoteParameter param = method.parameters().get(name);
if (param != null && typeMatches(method, param, ctx, type)) {
if (param != null && typeMatches(method, param, rootSchema, path, type)) {
return param;
}
return null;
@ -802,30 +809,63 @@ public class TraceRmiTarget extends AbstractTarget {
static final List<ToggleBreakMatcher> SPEC = matchers(HAS_SPEC);
}
record MatchKey(Class<? extends MethodMatcher> cls, ActionName action, TraceObjectSchema sch) {}
protected class Matches {
private final Map<String, MatchedMethod> map = new HashMap<>();
private final Map<MatchKey, MatchedMethod> map = new HashMap<>();
public MatchedMethod getBest(String name, ActionName action,
Supplier<List<? extends MethodMatcher>> preferredSupplier) {
return getBest(name, action, preferredSupplier.get());
public MatchKey makeKey(Class<? extends MethodMatcher> cls, ActionName action,
KeyPath path) {
TraceObjectSchema rootSchema = trace.getObjectManager().getRootSchema();
if (rootSchema == null) {
return null;
}
return new MatchKey(cls, action,
path == null ? null : rootSchema.getSuccessorSchema(path));
}
public MatchedMethod getBest(String name, ActionName action,
List<? extends MethodMatcher> preferred) {
public <T extends MethodMatcher> MatchedMethod getBest(Class<T> cls, KeyPath path,
ActionName action, Supplier<List<T>> preferredSupplier) {
return getBest(cls, path, action, preferredSupplier.get());
}
/**
* Search for the most preferred method for a given operation, with respect to a given path
*
* <p>
* A given path should be given as a point of reference, usually the current object or the
* object from the UI action context. If given, parameters that require a certain
* {@link TraceObjectInterface} will seek a suitable schema from that path and require it.
* Otherwise, any parameter whose schema includes the interface will be accepted.
*
* @param <T> the matcher class representing the desired operation
* @param cls the matcher class representing the desired operation
* @param path a path as a point of reference, or null for "any" point of reference.
* @param action the required action name for a matching method
* @param preferred the list of matchers (signatures) in preferred order
* @return the best method, or null
*/
public <T extends MethodMatcher> MatchedMethod getBest(Class<T> cls, KeyPath path,
ActionName action, List<T> preferred) {
MatchKey key = makeKey(cls, action, path);
synchronized (map) {
return map.computeIfAbsent(name, n -> chooseBest(action, preferred));
return map.computeIfAbsent(key, k -> chooseBest(action, path, preferred));
}
}
private MatchedMethod chooseBest(ActionName name, List<? extends MethodMatcher> preferred) {
private MatchedMethod chooseBest(ActionName name, KeyPath path,
List<? extends MethodMatcher> preferred) {
if (preferred.isEmpty()) {
return null;
}
SchemaContext ctx = getSchemaContext();
TraceObjectSchema rootSchema = trace.getObjectManager().getRootSchema();
if (rootSchema == null) {
return null;
}
MatchedMethod best = connection.getMethods()
.getByAction(name)
.stream()
.map(m -> MethodMatcher.matchPreferredForm(m, ctx, preferred))
.map(m -> MethodMatcher.matchPreferredForm(m, rootSchema, path, preferred))
.filter(f -> f != null)
.max(MatchedMethod::compareTo)
.orElse(null);
@ -902,7 +942,8 @@ public class TraceRmiTarget extends AbstractTarget {
@Override
public CompletableFuture<String> executeAsync(String command, boolean toString) {
MatchedMethod execute = matches.getBest("execute", ActionName.EXECUTE, ExecuteMatcher.ALL);
MatchedMethod execute =
matches.getBest(ExecuteMatcher.class, null, ActionName.EXECUTE, ExecuteMatcher.ALL);
if (execute == null) {
return CompletableFuture.failedFuture(new NoSuchElementException());
}
@ -923,8 +964,8 @@ public class TraceRmiTarget extends AbstractTarget {
return AsyncUtils.nil();
}
SchemaName name = object.getSchema().getName();
MatchedMethod activate = matches.getBest("activate_" + name, ActionName.ACTIVATE,
MatchedMethod activate =
matches.getBest(ActivateMatcher.class, object.getCanonicalPath(), ActionName.ACTIVATE,
() -> ActivateMatcher.makeBySpecificity(trace.getObjectManager().getRootSchema(),
object.getCanonicalPath()));
if (activate == null) {
@ -1004,7 +1045,8 @@ public class TraceRmiTarget extends AbstractTarget {
// NOTE: I don't intend to warn about the number of requests.
// They're delivered in serial, and there's a cancel button that works
MatchedMethod readMem = matches.getBest("readMem", ActionName.READ_MEM, ReadMemMatcher.ALL);
MatchedMethod readMem =
matches.getBest(ReadMemMatcher.class, null, ActionName.READ_MEM, ReadMemMatcher.ALL);
if (readMem == null) {
return AsyncUtils.nil();
}
@ -1066,7 +1108,7 @@ public class TraceRmiTarget extends AbstractTarget {
@Override
public CompletableFuture<Void> writeMemoryAsync(Address address, byte[] data) {
MatchedMethod writeMem =
matches.getBest("writeMem", ActionName.WRITE_MEM, WriteMemMatcher.ALL);
matches.getBest(WriteMemMatcher.class, null, ActionName.WRITE_MEM, WriteMemMatcher.ALL);
if (writeMem == null) {
return AsyncUtils.nil();
}
@ -1088,7 +1130,7 @@ public class TraceRmiTarget extends AbstractTarget {
public CompletableFuture<Void> readRegistersAsync(TracePlatform platform, TraceThread thread,
int frame, Set<Register> registers) {
MatchedMethod readRegs =
matches.getBest("readRegs", ActionName.REFRESH, ReadRegsMatcher.ALL);
matches.getBest(ReadRegsMatcher.class, null, ActionName.REFRESH, ReadRegsMatcher.ALL);
if (readRegs == null) {
return AsyncUtils.nil();
}
@ -1218,7 +1260,7 @@ public class TraceRmiTarget extends AbstractTarget {
public CompletableFuture<Void> writeRegisterAsync(TracePlatform platform, TraceThread thread,
int frameLevel, RegisterValue value) {
MatchedMethod writeReg =
matches.getBest("writeReg", ActionName.WRITE_REG, WriteRegMatcher.ALL);
matches.getBest(WriteRegMatcher.class, null, ActionName.WRITE_REG, WriteRegMatcher.ALL);
if (writeReg == null) {
return AsyncUtils.nil();
}
@ -1371,8 +1413,8 @@ public class TraceRmiTarget extends AbstractTarget {
protected CompletableFuture<Void> placeHwExecBreakAsync(Address address, String condition,
String commands) {
MatchedMethod breakHwExec =
matches.getBest(BREAK_HW_EXEC, ActionName.BREAK_HW_EXECUTE, BreakExecMatcher.ALL);
MatchedMethod breakHwExec = matches.getBest(BreakExecMatcher.class, null,
ActionName.BREAK_HW_EXECUTE, BreakExecMatcher.ALL);
if (breakHwExec == null) {
return AsyncUtils.nil();
}
@ -1381,8 +1423,8 @@ public class TraceRmiTarget extends AbstractTarget {
protected CompletableFuture<Void> placeSwExecBreakAsync(Address address, String condition,
String commands) {
MatchedMethod breakSwExec =
matches.getBest(BREAK_SW_EXEC, ActionName.BREAK_SW_EXECUTE, BreakExecMatcher.ALL);
MatchedMethod breakSwExec = matches.getBest(BreakExecMatcher.class, null,
ActionName.BREAK_SW_EXECUTE, BreakExecMatcher.ALL);
if (breakSwExec == null) {
return AsyncUtils.nil();
}
@ -1399,8 +1441,8 @@ public class TraceRmiTarget extends AbstractTarget {
protected CompletableFuture<Void> placeReadBreakAsync(AddressRange range, String condition,
String commands) {
MatchedMethod breakRead =
matches.getBest(BREAK_READ, ActionName.BREAK_READ, BreakAccMatcher.ALL);
MatchedMethod breakRead = matches.getBest(BreakAccMatcher.class, null,
ActionName.BREAK_READ, BreakAccMatcher.ALL);
if (breakRead == null) {
return AsyncUtils.nil();
}
@ -1409,8 +1451,8 @@ public class TraceRmiTarget extends AbstractTarget {
protected CompletableFuture<Void> placeWriteBreakAsync(AddressRange range, String condition,
String commands) {
MatchedMethod breakWrite =
matches.getBest(BREAK_WRITE, ActionName.BREAK_WRITE, BreakAccMatcher.ALL);
MatchedMethod breakWrite = matches.getBest(BreakAccMatcher.class, null,
ActionName.BREAK_WRITE, BreakAccMatcher.ALL);
if (breakWrite == null) {
return AsyncUtils.nil();
}
@ -1419,8 +1461,8 @@ public class TraceRmiTarget extends AbstractTarget {
protected CompletableFuture<Void> placeAccessBreakAsync(AddressRange range, String condition,
String commands) {
MatchedMethod breakAccess =
matches.getBest(BREAK_ACCESS, ActionName.BREAK_ACCESS, BreakAccMatcher.ALL);
MatchedMethod breakAccess = matches.getBest(BreakAccMatcher.class, null,
ActionName.BREAK_ACCESS, BreakAccMatcher.ALL);
if (breakAccess == null) {
return AsyncUtils.nil();
}
@ -1484,15 +1526,16 @@ public class TraceRmiTarget extends AbstractTarget {
if (breakpoint.getName().endsWith("emu-" + breakpoint.getMinAddress())) {
return false;
}
if (!breakpoint.getLifespan().contains(getSnap())) {
if (!breakpoint.isAlive(getSnap())) {
return false;
}
return true;
}
protected CompletableFuture<Void> deleteBreakpointSpecAsync(TraceObjectBreakpointSpec spec) {
KeyPath path = spec.getObject().getCanonicalPath();
MatchedMethod delBreak =
matches.getBest("delBreakSpec", ActionName.DELETE, DelBreakMatcher.SPEC);
matches.getBest(DelBreakMatcher.class, path, ActionName.DELETE, DelBreakMatcher.SPEC);
if (delBreak == null) {
return AsyncUtils.nil();
}
@ -1504,8 +1547,9 @@ public class TraceRmiTarget extends AbstractTarget {
// TODO: Would this make sense for any debugger? To delete individual locations?
protected CompletableFuture<Void> deleteBreakpointLocAsync(TraceObjectBreakpointLocation loc) {
KeyPath path = loc.getObject().getCanonicalPath();
MatchedMethod delBreak =
matches.getBest("delBreakLoc", ActionName.DELETE, DelBreakMatcher.ALL);
matches.getBest(DelBreakMatcher.class, path, ActionName.DELETE, DelBreakMatcher.ALL);
if (delBreak == null) {
Msg.debug(this, "Falling back to delete spec");
return deleteBreakpointSpecAsync(loc.getSpecification());
@ -1533,33 +1577,37 @@ public class TraceRmiTarget extends AbstractTarget {
protected CompletableFuture<Void> toggleBreakpointSpecAsync(TraceObjectBreakpointSpec spec,
boolean enabled) {
MatchedMethod delBreak =
matches.getBest("toggleBreakSpec", ActionName.TOGGLE, ToggleBreakMatcher.SPEC);
if (delBreak == null) {
KeyPath path = spec.getObject().getCanonicalPath();
MatchedMethod toggleBreak =
matches.getBest(ToggleBreakMatcher.class, path, ActionName.TOGGLE,
ToggleBreakMatcher.SPEC);
if (toggleBreak == null) {
return AsyncUtils.nil();
}
return delBreak.method
return toggleBreak.method
.invokeAsync(Map.ofEntries(
Map.entry(delBreak.params.get("specification").name(), spec.getObject()),
Map.entry(delBreak.params.get("enabled").name(), enabled)))
Map.entry(toggleBreak.params.get("specification").name(), spec.getObject()),
Map.entry(toggleBreak.params.get("enabled").name(), enabled)))
.toCompletableFuture()
.thenApply(__ -> null);
}
protected CompletableFuture<Void> toggleBreakpointLocAsync(TraceObjectBreakpointLocation loc,
boolean enabled) {
MatchedMethod delBreak =
matches.getBest("toggleBreakLoc", ActionName.TOGGLE, ToggleBreakMatcher.ALL);
if (delBreak == null) {
KeyPath path = loc.getObject().getCanonicalPath();
MatchedMethod toggleBreak =
matches.getBest(ToggleBreakMatcher.class, path, ActionName.TOGGLE,
ToggleBreakMatcher.ALL);
if (toggleBreak == null) {
Msg.debug(this, "Falling back to toggle spec");
return toggleBreakpointSpecAsync(loc.getSpecification(), enabled);
}
RemoteParameter paramLocation = delBreak.params.get("location");
RemoteParameter paramLocation = toggleBreak.params.get("location");
if (paramLocation != null) {
return delBreak.method
return toggleBreak.method
.invokeAsync(Map.ofEntries(
Map.entry(paramLocation.name(), loc.getObject()),
Map.entry(delBreak.params.get("enabled").name(), enabled)))
Map.entry(toggleBreak.params.get("enabled").name(), enabled)))
.toCompletableFuture()
.thenApply(__ -> null);
}

View file

@ -623,8 +623,7 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter
private boolean isVisible(TraceBreakpoint location) {
long snap = traceManager.getCurrentFor(trace).getSnap();
Lifespan span = location.getLifespan();
return span != null && span.contains(snap);
return location.isAlive(snap);
}
private void locationAdded(TraceBreakpoint location) {

View file

@ -709,7 +709,7 @@ public class ObjectTreeModel implements DisplaysModified {
if (forType != null) {
return forType;
}
if (type.contains("Breakpoint")) {
if (type.contains("Breakpoint") || type.contains("Watchpoint")) {
TraceObject object = edge.getChild();
TraceObjectValue en =
object.getAttribute(snap, TraceObjectTogglable.KEY_ENABLED);

View file

@ -219,8 +219,7 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin
}
private void breakpointAdded(TraceBreakpoint tb) {
Lifespan span = tb.getLifespan();
if (span == null || !span.contains(info.snap)) {
if (!tb.isAlive(info.snap)) {
return;
}
try {
@ -233,7 +232,7 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin
}
private void breakpointChanged(TraceBreakpoint tb) {
if (!tb.getLifespan().contains(info.snap)) {
if (!tb.isAlive(info.snap)) {
return;
}
try {
@ -521,7 +520,7 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin
forgetTraceBreakpoint(r, tb);
continue;
}
if (!tb.getLifespan().contains(snap)) {
if (!tb.isAlive(snap)) {
forgetTraceBreakpoint(r, tb);
continue;
}

View file

@ -272,6 +272,13 @@ public class DBTraceBreakpoint
}
}
@Override
public boolean isAlive(long snap) {
try (LockHold hold = LockHold.lock(space.lock.readLock())) {
return lifespan.contains(snap);
}
}
@Override
public long getPlacedSnap() {
try (LockHold hold = LockHold.lock(space.lock.readLock())) {

View file

@ -196,6 +196,13 @@ public class DBTraceObjectBreakpointLocation
}
}
@Override
public boolean isAlive(long snap) {
try (LockHold hold = object.getTrace().lockRead()) {
return object.isAlive(snap);
}
}
@Override
public Lifespan computeSpan() {
Lifespan span = TraceObjectBreakpointLocation.super.computeSpan();

View file

@ -106,6 +106,13 @@ public class DBTraceObjectBreakpointSpec
return computeSpan();
}
@Override
public boolean isAlive(long snap) {
try (LockHold hold = object.getTrace().lockRead()) {
return object.isAlive(snap);
}
}
@Override
public long getPlacedSnap() {
return computeMinSnap();

View file

@ -163,23 +163,36 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
}
}
@Override
public LifeSet getLife() {
try (LockHold hold = manager.trace.lockRead()) {
protected LifeSet ensureCachedLife() {
if (cachedLife != null) {
synchronized (cachedLife) {
return DefaultLifeSet.copyOf(cachedLife);
}
return cachedLife;
}
MutableLifeSet result = new DefaultLifeSet();
getCanonicalParents(Lifespan.ALL).forEach(v -> result.add(v.getLifespan()));
cachedLife = result;
return result;
}
@Override
public LifeSet getLife() {
try (LockHold hold = manager.trace.lockRead()) {
LifeSet result = ensureCachedLife();
synchronized (result) {
return DefaultLifeSet.copyOf(result);
}
}
}
@Override
public boolean isAlive(long snap) {
try (LockHold hold = manager.trace.lockRead()) {
LifeSet result = ensureCachedLife();
synchronized (result) {
return result.contains(snap);
}
}
}
protected DBTraceObject doCreateCanonicalParentObject() {
return manager.doCreateObject(path.parent());
}

View file

@ -23,6 +23,7 @@ import ghidra.pcode.exec.SleighUtils;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;
import ghidra.trace.model.*;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.model.thread.TraceThread;
import ghidra.util.exception.DuplicateNameException;
@ -105,9 +106,21 @@ public interface TraceBreakpoint extends TraceUniqueObject {
* Get the lifespan of this breakpoint
*
* @return the lifespan
* @deprecated Either this method no longer makes sense, or we need to wrap a
* {@link TraceObjectValue} instead. Even then, the attribute values can vary over
* the lifespan.
*/
@Deprecated(since = "11.3", forRemoval = true)
Lifespan getLifespan();
/**
* Check if the breakpoint is present at the given snap
*
* @param snap the snap
* @return true if alive, false if not
*/
boolean isAlive(long snap);
/**
* Get the placed snap of this breakpoint
*

View file

@ -180,6 +180,17 @@ public interface TraceObject extends TraceUniqueObject {
*/
LifeSet getLife();
/**
* Check if the object is alive at the given snap
*
* <p>
* This is preferable to {@link #getLife()}, when we only need to check one snap
*
* @param snap the snap
* @return true if alive, false if not
*/
boolean isAlive(long snap);
/**
* Inserts this object at its canonical path for the given lifespan
*

View file

@ -497,18 +497,23 @@ public interface TraceObjectSchema {
public void nextLevel() {
Set<T> nextLevel = new HashSet<>();
for (T ent : allOnLevel) {
if (!descend(ent)) {
continue;
}
if (descendAttributes(ent)) {
expandAttributes(nextLevel, ent);
expandDefaultAttribute(nextLevel, ent);
}
if (descendElements(ent)) {
expandElements(nextLevel, ent);
expandDefaultElement(nextLevel, ent);
}
}
allOnLevel = nextLevel;
}
public boolean descend(T ent) {
public boolean descendAttributes(T ent) {
return true;
}
public boolean descendElements(T ent) {
return true;
}
@ -549,10 +554,15 @@ public interface TraceObjectSchema {
}
@Override
public boolean descend(SearchEntry ent) {
public boolean descendAttributes(SearchEntry ent) {
return ent.schema.getInterfaces().contains(TraceObjectAggregate.class);
}
@Override
public boolean descendElements(SearchEntry ent) {
return ent.schema.isCanonicalContainer();
}
@Override
public void expandAttribute(Set<SearchEntry> nextLevel, SearchEntry ent,
TraceObjectSchema schema, KeyPath path) {

View file

@ -16,12 +16,15 @@
package ghidra.dbg.target.schema;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.io.IOException;
import org.jdom.JDOMException;
import org.junit.Test;
import ghidra.trace.model.breakpoint.TraceObjectBreakpointLocation;
import ghidra.trace.model.target.path.KeyPath;
import ghidra.trace.model.target.schema.*;
import ghidra.trace.model.target.schema.DefaultTraceObjectSchema.DefaultAttributeSchema;
import ghidra.trace.model.target.schema.TraceObjectSchema.*;
@ -75,4 +78,35 @@ public class XmlTargetObjectSchemaTest {
SchemaContext result = XmlSchemaContext.deserialize(SCHEMA_XML);
assertEquals(CTX, result);
}
@Test
public void testSearchWithMultipleImpls() throws Exception {
SchemaContext ctx = XmlSchemaContext.deserialize("""
<context>
<schema name="root">
<interface name="Aggregate" />
<attribute name="Watches" schema="WatchContainer" />
<attribute name="Breaks" schema="BreakContainer" />
</schema>
<schema name="WatchContainer" canonical="yes">
<element schema="Watch" />
</schema>
<schema name="Watch">
<interface name="BreakpointSpec" />
<interface name="BreakpointLocation" />
</schema>
<schema name="BreakContainer" canonical="yes">
<element schema="Break" />
</schema>
<schema name="Break">
<interface name="BreakpointSpec" />
<interface name="BreakpointLocation" />
</schema>
</context>
""");
KeyPath found = ctx.getSchema(new SchemaName("root"))
.searchForSuitable(TraceObjectBreakpointLocation.class, KeyPath.ROOT);
assertNotNull(found);
}
}