GP-3984: Have gdb connector delay section info until asked.

This commit is contained in:
Dan 2024-02-01 08:50:27 -05:00
parent 683dfc6da3
commit 644e2c53e5
22 changed files with 301 additions and 141 deletions

View file

@ -467,11 +467,14 @@ def ghidra_trace_set_snap(snap, *, is_mi, **kwargs):
STATE.require_trace().set_snap(int(gdb.parse_and_eval(snap)))
def quantize_pages(start, end):
return (start // PAGE_SIZE * PAGE_SIZE, (end+PAGE_SIZE-1) // PAGE_SIZE*PAGE_SIZE)
def put_bytes(start, end, pages, is_mi, from_tty):
trace = STATE.require_trace()
if pages:
start = start // PAGE_SIZE * PAGE_SIZE
end = (end + PAGE_SIZE - 1) // PAGE_SIZE * PAGE_SIZE
start, end = quantize_pages(start, end)
inf = gdb.selected_inferior()
buf = bytes(inf.read_memory(start, end - start))
@ -486,6 +489,8 @@ def put_bytes(start, end, pages, is_mi, from_tty):
def eval_address(address):
if isinstance(address, int):
return address
try:
return int(gdb.parse_and_eval(address))
except gdb.error as e:
@ -494,10 +499,13 @@ def eval_address(address):
def eval_range(address, length):
start = eval_address(address)
try:
end = start + int(gdb.parse_and_eval(length))
except gdb.error as e:
raise gdb.GdbError("Cannot convert '{}' to length".format(length))
if isinstance(length, int):
end = start + length
else:
try:
end = start + int(gdb.parse_and_eval(length))
except gdb.error as e:
raise gdb.GdbError("Cannot convert '{}' to length".format(length))
return start, end
@ -532,6 +540,18 @@ def ghidra_trace_putval(value, pages=True, *, is_mi, from_tty=True, **kwargs):
return put_bytes(start, end, pages, is_mi, from_tty)
def putmem_state(address, length, state, pages=True):
STATE.trace.validate_state(state)
start, end = eval_range(address, length)
if pages:
start, end = quantize_pages(start, end)
inf = gdb.selected_inferior()
base, addr = STATE.trace.memory_mapper.map(inf, start)
if base != addr.space:
trace.create_overlay_space(base, addr.space)
STATE.trace.set_memory_state(addr.extend(end - start), state)
@cmd('ghidra trace putmem-state', '-ghidra-trace-putmem-state', gdb.COMMAND_DATA, True)
def ghidra_trace_putmem_state(address, length, state, *, is_mi, **kwargs):
"""
@ -539,13 +559,7 @@ def ghidra_trace_putmem_state(address, length, state, *, is_mi, **kwargs):
"""
STATE.require_tx()
STATE.trace.validate_state(state)
start, end = eval_range(address, length)
inf = gdb.selected_inferior()
base, addr = STATE.trace.memory_mapper.map(inf, start)
if base != addr.space:
trace.create_overlay_space(base, addr.space)
STATE.trace.set_memory_state(addr.extend(end - start), state)
putmem_state(address, length, state, True)
@cmd('ghidra trace delmem', '-ghidra-trace-delmem', gdb.COMMAND_DATA, True)
@ -1223,9 +1237,10 @@ def ghidra_trace_put_regions(*, is_mi, **kwargs):
put_regions()
def put_modules():
def put_modules(modules=None, sections=False):
inf = gdb.selected_inferior()
modules = util.MODULE_INFO_READER.get_modules()
if modules is None:
modules = util.MODULE_INFO_READER.get_modules()
mapper = STATE.trace.memory_mapper
mod_keys = []
for mk, m in modules.items():
@ -1237,29 +1252,32 @@ def put_modules():
if base_base != base_addr.space:
STATE.trace.create_overlay_space(base_base, base_addr.space)
modobj.set_value('_range', base_addr.extend(m.max - m.base))
sec_keys = []
for sk, s in m.sections.items():
spath = mpath + SECTION_ADD_PATTERN.format(secname=sk)
secobj = STATE.trace.create_object(spath)
sec_keys.append(SECTION_KEY_PATTERN.format(secname=sk))
start_base, start_addr = mapper.map(inf, s.start)
if start_base != start_addr.space:
STATE.trace.create_overlay_space(
start_base, start_addr.space)
secobj.set_value('_range', start_addr.extend(s.end - s.start))
secobj.set_value('_offset', s.offset)
secobj.set_value('_attrs', s.attrs, schema=sch.STRING_ARR)
secobj.insert()
# In case there are no sections, we must still insert the module
modobj.insert()
STATE.trace.proxy_object_path(
mpath + SECTIONS_ADD_PATTERN).retain_values(sec_keys)
STATE.trace.proxy_object_path(MODULES_PATTERN.format(
infnum=inf.num)).retain_values(mod_keys)
if sections:
sec_keys = []
for sk, s in m.sections.items():
spath = mpath + SECTION_ADD_PATTERN.format(secname=sk)
secobj = STATE.trace.create_object(spath)
sec_keys.append(SECTION_KEY_PATTERN.format(secname=sk))
start_base, start_addr = mapper.map(inf, s.start)
if start_base != start_addr.space:
STATE.trace.create_overlay_space(
start_base, start_addr.space)
secobj.set_value('_range', start_addr.extend(s.end - s.start))
secobj.set_value('_offset', s.offset)
secobj.set_value('_attrs', s.attrs, schema=sch.STRING_ARR)
secobj.insert()
STATE.trace.proxy_object_path(
mpath + SECTIONS_ADD_PATTERN).retain_values(sec_keys)
scpath = mpath + SECTIONS_ADD_PATTERN
sec_container_obj = STATE.trace.create_object(scpath)
sec_container_obj.insert()
if not sections:
STATE.trace.proxy_object_path(MODULES_PATTERN.format(
infnum=inf.num)).retain_values(mod_keys)
@cmd('ghidra trace put-modules', '-ghidra-trace-put-modules', gdb.COMMAND_DATA,
True)
@cmd('ghidra trace put-modules', '-ghidra-trace-put-modules', gdb.COMMAND_DATA, True)
def ghidra_trace_put_modules(*, is_mi, **kwargs):
"""
Gather object files, if applicable, and write to the trace's Modules.
@ -1270,6 +1288,25 @@ def ghidra_trace_put_modules(*, is_mi, **kwargs):
put_modules()
@cmd('ghidra trace put-sections', '-ghidra-trace-put-sections', gdb.COMMAND_DATA, True)
def ghidra_trace_put_sections(module_name, *, is_mi, **kwargs):
"""
Write the sections of the given module or all modules
"""
modules = None
if module_name != '-all-objects':
modules = {mk: m for mk, m in util.MODULE_INFO_READER.get_modules(
).items() if mk == module_name}
if len(modules) == 0:
raise gdb.GdbError(
"No module / object named {}".format(module_name))
STATE.require_tx()
with STATE.client.batch() as b:
put_modules(modules, True)
def convert_state(t):
if t.is_exited():
return 'TERMINATED'
@ -1405,8 +1442,8 @@ def ghidra_trace_put_all(*, is_mi, **kwargs):
ghidra_trace_putmem("$sp", "1", is_mi=is_mi)
put_inferiors()
put_environment()
put_regions()
put_modules()
put_regions()
put_threads()
put_frames()
put_breakpoints()

View file

@ -96,9 +96,11 @@ class InferiorState(object):
self.visited.add(hashable_frame)
if first or self.regions or self.threads or self.modules:
# Sections, memory syscalls, or stack allocations
commands.put_modules()
self.modules = False
commands.put_regions()
self.regions = False
if first or self.modules:
elif first or self.modules:
commands.put_modules()
self.modules = False
if first or self.breaks:

View file

@ -78,6 +78,7 @@ FRAME_PATTERN = extre(STACK_PATTERN, '\[(?P<level>\\d*)\]')
REGS_PATTERN = extre(FRAME_PATTERN, '.Registers')
MEMORY_PATTERN = extre(INFERIOR_PATTERN, '\.Memory')
MODULES_PATTERN = extre(INFERIOR_PATTERN, '\.Modules')
MODULE_PATTERN = extre(MODULES_PATTERN, '\[(?P<modname>.*)\]')
def find_availpid_by_pattern(pattern, object, err_msg):
@ -132,6 +133,17 @@ def find_inf_by_modules_obj(object):
return find_inf_by_pattern(object, MODULES_PATTERN, "a ModuleContainer")
def find_inf_by_mod_obj(object):
return find_inf_by_pattern(object, MODULE_PATTERN, "a Module")
def find_module_name_by_mod_obj(object):
mat = MODULE_PATTERN.fullmatch(object.path)
if mat is None:
raise TypeError(f"{object} is not a Module")
return mat['modname']
def find_thread_by_num(inf, tnum):
for t in inf.threads():
if t.num == tnum:
@ -257,7 +269,7 @@ def find_bpt_loc_by_obj(object):
def switch_inferior(inferior):
if gdb.selected_inferior().num == inferior.num:
return
gdb.execute("inferior {}".format(inferior.num))
gdb.execute(f'inferior {inferior.num}')
@REGISTRY.method
@ -266,14 +278,14 @@ def execute(cmd: str, to_string: bool=False):
return gdb.execute(cmd, to_string=to_string)
@REGISTRY.method(action='refresh')
@REGISTRY.method(action='refresh', display='Refresh Available')
def refresh_available(node: sch.Schema('AvailableContainer')):
"""List processes on gdb's host system."""
with commands.open_tracked_tx('Refresh Available'):
gdb.execute('ghidra trace put-available')
@REGISTRY.method(action='refresh')
@REGISTRY.method(action='refresh', display='Refresh Breakpoints')
def refresh_breakpoints(node: sch.Schema('BreakpointContainer')):
"""
Refresh the list of breakpoints (including locations for the current
@ -283,14 +295,14 @@ def refresh_breakpoints(node: sch.Schema('BreakpointContainer')):
gdb.execute('ghidra trace put-breakpoints')
@REGISTRY.method(action='refresh')
@REGISTRY.method(action='refresh', display='Refresh Inferiors')
def refresh_inferiors(node: sch.Schema('InferiorContainer')):
"""Refresh the list of inferiors."""
with commands.open_tracked_tx('Refresh Inferiors'):
gdb.execute('ghidra trace put-inferiors')
@REGISTRY.method(action='refresh')
@REGISTRY.method(action='refresh', display='Refresh Breakpoint Locations')
def refresh_inf_breakpoints(node: sch.Schema('BreakpointLocationContainer')):
"""
Refresh the breakpoint locations for the inferior.
@ -303,7 +315,7 @@ def refresh_inf_breakpoints(node: sch.Schema('BreakpointLocationContainer')):
gdb.execute('ghidra trace put-breakpoints')
@REGISTRY.method(action='refresh')
@REGISTRY.method(action='refresh', display='Refresh Environment')
def refresh_environment(node: sch.Schema('Environment')):
"""Refresh the environment descriptors (arch, os, endian)."""
switch_inferior(find_inf_by_env_obj(node))
@ -311,7 +323,7 @@ def refresh_environment(node: sch.Schema('Environment')):
gdb.execute('ghidra trace put-environment')
@REGISTRY.method(action='refresh')
@REGISTRY.method(action='refresh', display='Refresh Threads')
def refresh_threads(node: sch.Schema('ThreadContainer')):
"""Refresh the list of threads in the inferior."""
switch_inferior(find_inf_by_threads_obj(node))
@ -319,7 +331,7 @@ def refresh_threads(node: sch.Schema('ThreadContainer')):
gdb.execute('ghidra trace put-threads')
@REGISTRY.method(action='refresh')
@REGISTRY.method(action='refresh', display='Refresh Stack')
def refresh_stack(node: sch.Schema('Stack')):
"""Refresh the backtrace for the thread."""
find_thread_by_stack_obj(node).switch()
@ -327,7 +339,7 @@ def refresh_stack(node: sch.Schema('Stack')):
gdb.execute('ghidra trace put-frames')
@REGISTRY.method(action='refresh')
@REGISTRY.method(action='refresh', display='Refresh Registers')
def refresh_registers(node: sch.Schema('RegisterValueContainer')):
"""Refresh the register values for the frame."""
find_frame_by_regs_obj(node).select()
@ -336,7 +348,7 @@ def refresh_registers(node: sch.Schema('RegisterValueContainer')):
gdb.execute('ghidra trace putreg')
@REGISTRY.method(action='refresh')
@REGISTRY.method(action='refresh', display='Refresh Memory')
def refresh_mappings(node: sch.Schema('Memory')):
"""Refresh the list of memory regions for the inferior."""
switch_inferior(find_inf_by_mem_obj(node))
@ -344,18 +356,38 @@ def refresh_mappings(node: sch.Schema('Memory')):
gdb.execute('ghidra trace put-regions')
@REGISTRY.method(action='refresh')
@REGISTRY.method(action='refresh', display="Refresh Modules")
def refresh_modules(node: sch.Schema('ModuleContainer')):
"""
Refresh the modules and sections list for the inferior.
This will refresh the sections for all modules, not just the selected one.
Refresh the modules list for the inferior.
"""
switch_inferior(find_inf_by_modules_obj(node))
with commands.open_tracked_tx('Refresh Modules'):
gdb.execute('ghidra trace put-modules')
# node is Module so this appears in Modules panel
@REGISTRY.method(display='Load all Modules and all Sections')
def load_all_sections(node: sch.Schema('Module')):
"""
Load/refresh all modules and all sections.
"""
switch_inferior(find_inf_by_mod_obj(node))
with commands.open_tracked_tx('Refresh all Modules and all Sections'):
gdb.execute('ghidra trace put-sections -all-objects')
@REGISTRY.method(action='refresh', display="Refresh Module and Sections")
def refresh_sections(node: sch.Schema('Module')):
"""
Load/refresh the module and its sections.
"""
switch_inferior(find_inf_by_mod_obj(node))
with commands.open_tracked_tx('Refresh Module and Sections'):
modname = find_module_name_by_mod_obj(node)
gdb.execute(f'ghidra trace put-sections {modname}')
@REGISTRY.method(action='activate')
def activate_inferior(inferior: sch.Schema('Inferior')):
"""Switch to the inferior."""
@ -374,7 +406,7 @@ def activate_frame(frame: sch.Schema('StackFrame')):
find_frame_by_obj(frame).select()
@REGISTRY.method
@REGISTRY.method(display='Add Inferior')
def add_inferior(container: sch.Schema('InferiorContainer')):
"""Add a new inferior."""
gdb.execute('add-inferior')
@ -388,14 +420,14 @@ def delete_inferior(inferior: sch.Schema('Inferior')):
# TODO: Separate method for each of core, exec, remote, etc...?
@REGISTRY.method
@REGISTRY.method(display='Connect Target')
def connect(inferior: sch.Schema('Inferior'), spec: str):
"""Connect to a target machine or process."""
switch_inferior(find_inf_by_obj(inferior))
gdb.execute(f'target {spec}')
@REGISTRY.method(action='attach')
@REGISTRY.method(action='attach', display='Attach by Available')
def attach_obj(inferior: sch.Schema('Inferior'), target: sch.Schema('Attachable')):
"""Attach the inferior to the given target."""
switch_inferior(find_inf_by_obj(inferior))
@ -403,21 +435,21 @@ def attach_obj(inferior: sch.Schema('Inferior'), target: sch.Schema('Attachable'
gdb.execute(f'attach {pid}')
@REGISTRY.method(action='attach')
@REGISTRY.method(action='attach', display='Attach by PID')
def attach_pid(inferior: sch.Schema('Inferior'), pid: int):
"""Attach the inferior to the given target."""
switch_inferior(find_inf_by_obj(inferior))
gdb.execute(f'attach {pid}')
@REGISTRY.method
@REGISTRY.method(display='Detach')
def detach(inferior: sch.Schema('Inferior')):
"""Detach the inferior's target."""
switch_inferior(find_inf_by_obj(inferior))
gdb.execute('detach')
@REGISTRY.method(action='launch')
@REGISTRY.method(action='launch', display='Launch at main')
def launch_main(inferior: sch.Schema('Inferior'),
file: ParamDesc(str, display='File'),
args: ParamDesc(str, display='Arguments')=''):
@ -435,7 +467,8 @@ def launch_main(inferior: sch.Schema('Inferior'),
''')
@REGISTRY.method(action='launch', condition=util.GDB_VERSION.major >= 9)
@REGISTRY.method(action='launch', display='Launch at Loader',
condition=util.GDB_VERSION.major >= 9)
def launch_loader(inferior: sch.Schema('Inferior'),
file: ParamDesc(str, display='File'),
args: ParamDesc(str, display='Arguments')=''):
@ -451,7 +484,7 @@ def launch_loader(inferior: sch.Schema('Inferior'),
''')
@REGISTRY.method(action='launch')
@REGISTRY.method(action='launch', display='Land and Run')
def launch_run(inferior: sch.Schema('Inferior'),
file: ParamDesc(str, display='File'),
args: ParamDesc(str, display='Arguments')=''):
@ -514,7 +547,7 @@ def step_out(thread: sch.Schema('Thread')):
gdb.execute('finish')
@REGISTRY.method(action='step_ext', name='Advance')
@REGISTRY.method(action='step_ext', display='Advance')
def step_advance(thread: sch.Schema('Thread'), address: Address):
"""Continue execution up to the given address (advance)."""
t = find_thread_by_obj(thread)
@ -523,7 +556,7 @@ def step_advance(thread: sch.Schema('Thread'), address: Address):
gdb.execute(f'advance *0x{offset:x}')
@REGISTRY.method(action='step_ext', name='Return')
@REGISTRY.method(action='step_ext', display='Return')
def step_return(thread: sch.Schema('Thread'), value: int=None):
"""Skip the remainder of the current function (return)."""
find_thread_by_obj(thread).switch()
@ -611,7 +644,7 @@ def break_access_expression(expression: str):
gdb.execute(f'awatch {expression}')
@REGISTRY.method(action='break_ext')
@REGISTRY.method(action='break_ext', display='Catch Event')
def break_event(spec: str):
"""Set a catchpoint (catch)."""
gdb.execute(f'catch {spec}')
@ -653,7 +686,12 @@ def read_mem(inferior: sch.Schema('Inferior'), range: AddressRange):
offset_start = inferior.trace.memory_mapper.map_back(
inf, Address(range.space, range.min))
with commands.open_tracked_tx('Read Memory'):
gdb.execute(f'ghidra trace putmem 0x{offset_start:x} {range.length()}')
try:
gdb.execute(
f'ghidra trace putmem 0x{offset_start:x} {range.length()}')
except:
commands.putmem_state(
offset_start, offset_start+range.length() - 1, 'error')
@REGISTRY.method

View file

@ -33,6 +33,10 @@ import java.util.Map;
* list may change over time, but that shouldn't matter much. Each back-end should make its best
* effort to match its methods to these stock actions where applicable, but ultimately, it is up to
* the UI to decide what is presented where.
*
* @param name the name of the action (given as the action attribute on method annotations)
* @param builtIn true if the action should <em>not</em> be presented in generic contexts, but
* reserved for built-in, purpose-specific actions
*/
public record ActionName(String name, boolean builtIn) {
private static final Map<String, ActionName> NAMES = new HashMap<>();
@ -63,7 +67,7 @@ public record ActionName(String name, boolean builtIn) {
}
}
public static final ActionName REFRESH = builtIn("refresh");
public static final ActionName REFRESH = extended("refresh");
/**
* Activate a given object and optionally a time
*

View file

@ -63,11 +63,14 @@ public interface Target {
* @param name the name of a common debugger command this action implements
* @param details text providing more details, usually displayed in a tool tip
* @param requiresPrompt true if invoking the action requires further user interaction
* @param specificity a relative score of specificity. These are only meaningful when compared
* among entries returned in the same collection.
* @param enabled a supplier to determine whether an associated action in the UI is enabled.
* @param action a function for invoking this action asynchronously
*/
record ActionEntry(String display, ActionName name, String details, boolean requiresPrompt,
BooleanSupplier enabled, Function<Boolean, CompletableFuture<?>> action) {
long specificity, BooleanSupplier enabled,
Function<Boolean, CompletableFuture<?>> action) {
/**
* Check if this action is currently enabled

View file

@ -56,6 +56,13 @@ public interface RemoteMethod {
*/
ActionName action();
/**
* A title to display in the UI for this action.
*
* @return the title
*/
String display();
/**
* A description of the method.
*

View file

@ -23,8 +23,8 @@ import ghidra.debug.api.tracermi.*;
import ghidra.trace.model.Trace;
public record RecordRemoteMethod(TraceRmiHandler handler, String name, ActionName action,
String description, Map<String, RemoteParameter> parameters, SchemaName retType)
implements RemoteMethod {
String display, String description, Map<String, RemoteParameter> parameters,
SchemaName retType) implements RemoteMethod {
@Override
public DefaultRemoteAsyncResult invokeAsync(Map<String, Object> arguments) {
Trace trace = validate(arguments);

View file

@ -484,10 +484,10 @@ public class TraceRmiHandler implements TraceRmiConnection {
RootMessage.Builder dispatch(RootMessage req, RootMessage.Builder rep) throws Exception;
default RootMessage handle(RootMessage req) {
String desc = toString(req);
/*String desc = toString(req);
if (desc != null) {
TimedMsg.debug(this, "HANDLING: " + desc);
}
}*/
RootMessage.Builder rep = RootMessage.newBuilder();
try {
rep = dispatch(req, rep);
@ -814,7 +814,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
protected ReplyActivate handleActivate(RequestActivate req) {
OpenTrace open = requireOpenTrace(req.getOid());
TraceObject object = open.getObject(req.getObject(), true);
TraceObject object = open.getObject(req.getObject(), false);
DebuggerCoordinates coords = traceManager.getCurrent();
if (coords.getTrace() != open.trace) {
coords = DebuggerCoordinates.NOWHERE;
@ -822,7 +822,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
if (open.lastSnapshot != null && followsPresent(open.trace)) {
coords = coords.snap(open.lastSnapshot.getKey());
}
DebuggerCoordinates finalCoords = coords.object(object);
DebuggerCoordinates finalCoords = object == null ? coords : coords.object(object);
Swing.runLater(() -> {
if (!traceManager.getOpenTraces().contains(open.trace)) {
traceManager.openTrace(open.trace);
@ -1031,7 +1031,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
}
for (Method m : req.getMethodsList()) {
RemoteMethod rm = new RecordRemoteMethod(this, m.getName(),
ActionName.name(m.getAction()),
ActionName.name(m.getAction()), m.getDisplay(),
m.getDescription(), m.getParametersList()
.stream()
.collect(Collectors.toMap(MethodParameter::getName, this::makeParameter)),

View file

@ -240,6 +240,16 @@ public class TraceRmiTarget extends AbstractTarget {
}
}
protected long computeSpecificity(Map<String, Object> args) {
long score = 0;
for (Object o : args.values()) {
if (o instanceof TraceObject obj) {
score += obj.getCanonicalPath().getKeyList().size();
}
}
return score;
}
protected BooleanSupplier chooseEnabler(RemoteMethod method, Map<String, Object> args) {
ActionName name = method.action();
SchemaContext ctx = getSchemaContext();
@ -282,7 +292,7 @@ public class TraceRmiTarget extends AbstractTarget {
private Map<String, Object> promptArgs(RemoteMethod method, Map<String, Object> defaults) {
SchemaContext ctx = getSchemaContext();
RemoteMethodInvocationDialog dialog = new RemoteMethodInvocationDialog(tool,
method.name(), method.name(), null);
method.display(), method.display(), null);
while (true) {
for (RemoteParameter param : method.parameters().values()) {
Object val = defaults.get(param.name());
@ -325,8 +335,9 @@ public class TraceRmiTarget extends AbstractTarget {
Map<String, Object> args = collectArguments(method, context, allowContextObject,
allowCoordsObject, allowSuitableObject);
boolean requiresPrompt = args.values().contains(Missing.MISSING);
return new ActionEntry(method.name(), method.action(), method.description(), requiresPrompt,
chooseEnabler(method, args), prompt -> invokeMethod(prompt, method, args));
return new ActionEntry(method.display(), method.action(), method.description(),
requiresPrompt, computeSpecificity(args), chooseEnabler(method, args),
prompt -> invokeMethod(prompt, method, args));
}
protected Map<String, ActionEntry> collectFromMethods(Collection<RemoteMethod> methods,
@ -415,6 +426,11 @@ public class TraceRmiTarget extends AbstractTarget {
return collectByName(ActionName.STEP_EXT, context);
}
@Override
protected Map<String, ActionEntry> collectRefreshActions(ActionContext context) {
return collectByName(ActionName.REFRESH, context);
}
@Override
public boolean isSupportsFocus() {
TargetObjectSchema schema = trace.getObjectManager().getRootSchema();

View file

@ -22,6 +22,7 @@ import org.apache.commons.lang3.ArrayUtils;
import ghidra.program.model.address.*;
import ghidra.rmi.trace.TraceRmi.*;
import ghidra.util.NumericUtilities;
public interface ValueDecoder {
ValueDecoder DEFAULT = new ValueDecoder() {};
@ -104,7 +105,8 @@ public interface ValueDecoder {
case STRING_VALUE -> value.getStringValue();
case BOOL_ARR_VALUE -> ArrayUtils.toPrimitive(
value.getBoolArrValue().getArrList().stream().toArray(Boolean[]::new));
case BYTES_VALUE -> value.getBytesValue().toByteArray();
case BYTES_VALUE -> NumericUtilities
.convertBytesToString(value.getBytesValue().toByteArray(), ":");
case CHAR_ARR_VALUE -> value.getCharArrValue().toCharArray();
case SHORT_ARR_VALUE -> ArrayUtils.toPrimitive(
value.getShortArrValue()

View file

@ -417,11 +417,12 @@ message MethodArgument {
message Method {
string name = 1;
string action = 2;
string description = 3;
repeated MethodParameter parameters = 4;
string display = 3;
string description = 4;
repeated MethodParameter parameters = 5;
// I'd like to make them all void, but I think executing a command and capturing its output
// justifies being able to return a result. It should be used very sparingly.
ValueType return_type = 5;
ValueType return_type = 6;
}
message RequestNegotiate {

View file

@ -425,6 +425,7 @@ class ParamDesc:
class RemoteMethod:
name: str
action: str
display: str
description: str
parameters: List[RemoteParameter]
return_schema: sch.Schema
@ -484,11 +485,14 @@ class MethodRegistry(object):
cls._to_display(p.annotation), cls._to_description(p.annotation))
@classmethod
def create_method(cls, function, name=None, action=None, description=None) -> RemoteMethod:
def create_method(cls, function, name=None, action=None, display=None,
description=None) -> RemoteMethod:
if name is None:
name = function.__name__
if action is None:
action = name
if display is None:
display = name
if description is None:
description = function.__doc__ or ''
sig = inspect.signature(function)
@ -496,14 +500,16 @@ class MethodRegistry(object):
for p in sig.parameters.values():
params.append(cls._make_param(p))
return_schema = cls._to_schema(sig, sig.return_annotation)
return RemoteMethod(name, action, description, params, return_schema, function)
return RemoteMethod(name, action, display, description, params,
return_schema, function)
def method(self, func=None, *, name=None, action=None, description='',
condition=True):
def method(self, func=None, *, name=None, action=None, display=None,
description='', condition=True):
def _method(func):
if condition:
method = self.create_method(func, name, action, description)
method = self.create_method(func, name, action, display,
description)
self.register_method(method)
return func
@ -669,6 +675,7 @@ class Client(object):
def _write_method(to: bufs.Method, method: RemoteMethod):
to.name = method.name
to.action = method.action
to.display = method.display
to.description = method.description
Client._write_parameters(to.parameters, method.parameters)
to.return_type.name = method.return_schema.name

View file

@ -48,19 +48,19 @@ public class TestTraceRmiConnection implements TraceRmiConnection {
}
}
public record TestRemoteMethod(String name, ActionName action, String description,
Map<String, RemoteParameter> parameters, SchemaName retType,
public record TestRemoteMethod(String name, ActionName action, String display,
String description, Map<String, RemoteParameter> parameters, SchemaName retType,
AsyncPairingQueue<Map<String, Object>> argQueue, AsyncPairingQueue<Object> retQueue)
implements RemoteMethod {
public TestRemoteMethod(String name, ActionName action, String description,
public TestRemoteMethod(String name, ActionName action, String display, String description,
Map<String, RemoteParameter> parameters, SchemaName retType) {
this(name, action, description, parameters, retType, new AsyncPairingQueue<>(),
this(name, action, display, description, parameters, retType, new AsyncPairingQueue<>(),
new AsyncPairingQueue<>());
}
public TestRemoteMethod(String name, ActionName action, String description,
public TestRemoteMethod(String name, ActionName action, String display, String description,
SchemaName retType, RemoteParameter... parameters) {
this(name, action, description, Stream.of(parameters)
this(name, action, display, description, Stream.of(parameters)
.collect(Collectors.toMap(RemoteParameter::name, p -> p)),
retType);
}

View file

@ -284,19 +284,6 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
updateActions();
}
protected void executeTask(Task task) {
if (progressService != null) {
progressService.execute(task);
}
else {
tool.execute(task);
}
}
protected void runTask(String title, ActionEntry entry) {
executeTask(new TargetActionTask(tool, title, entry));
}
protected void addTargetStepExtActions(Target target) {
for (ActionEntry entry : target.collectActions(ActionName.STEP_EXT, context).values()) {
if (entry.requiresPrompt()) {
@ -305,7 +292,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
actionsTargetStepExt.add(TargetStepExtAction.builder(entry.display(), this)
.description(entry.details())
.enabledWhen(ctx -> entry.isEnabled())
.onAction(ctx -> runTask(entry.display(), entry))
.onAction(ctx -> TargetActionTask.runAction(tool, entry.display(), entry))
.build());
}
}
@ -370,7 +357,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
if (target == null) {
return;
}
executeTask(new Task("Disconnect", false, false, false) {
TargetActionTask.executeTask(tool, new Task("Disconnect", false, false, false) {
@Override
public void run(TaskMonitor monitor) throws CancelledException {
try {

View file

@ -18,6 +18,8 @@ package ghidra.app.plugin.core.debug.gui.control;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Icon;
import docking.ActionContext;
import docking.Tool;
import docking.action.*;
@ -26,9 +28,11 @@ import ghidra.app.context.ProgramActionContext;
import ghidra.app.context.ProgramLocationActionContext;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext;
import ghidra.app.services.*;
import ghidra.debug.api.control.ControlMode;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.target.Target.ActionEntry;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
@ -59,12 +63,17 @@ public class DebuggerMethodActionsPlugin extends Plugin implements PopupActionPr
public InvokeActionEntryAction(ActionEntry entry) {
super(entry.display(), DebuggerMethodActionsPlugin.this.getName());
this.entry = entry;
setPopupMenuData(new MenuData(new String[] { getName() }, GROUP_METHODS));
Icon icon = null;
if (ActionName.REFRESH.equals(entry.name())) {
// TODO: Allow method annotation to specify icon?
icon = DebuggerResources.ICON_REFRESH;
}
setPopupMenuData(new MenuData(new String[] { getName() }, icon, GROUP_METHODS));
}
@Override
public void actionPerformed(ActionContext context) {
tool.execute(new TargetActionTask(tool, entry.display(), entry));
TargetActionTask.runAction(tool, entry.display(), entry);
}
}

View file

@ -18,17 +18,19 @@ package ghidra.app.plugin.core.debug.gui.control;
import docking.ActionContext;
import docking.action.builder.AbstractActionBuilder;
import ghidra.debug.api.target.ActionName;
import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.PluginTool;
class TargetActionBuilder
extends AbstractActionBuilder<TargetDockingAction, ActionContext, TargetActionBuilder> {
private final DebuggerControlPlugin plugin;
private final PluginTool tool;
private ActionName action;
private String defaultDescription;
public TargetActionBuilder(String name, DebuggerControlPlugin plugin) {
super(name, plugin.getName());
this.plugin = plugin;
public TargetActionBuilder(String name, Plugin owner) {
super(name, owner.getName());
this.tool = owner.getTool();
}
@Override
@ -62,7 +64,7 @@ class TargetActionBuilder
// Make the super.validate() hush
});
validate();
TargetDockingAction result = new TargetDockingAction(name, owner, keyBindingType, plugin,
TargetDockingAction result = new TargetDockingAction(name, owner, keyBindingType, tool,
action, defaultDescription);
decorateAction(result);
return result;

View file

@ -17,6 +17,7 @@ package ghidra.app.plugin.core.debug.gui.control;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.services.DebuggerConsoleService;
import ghidra.app.services.ProgressService;
import ghidra.debug.api.target.Target.ActionEntry;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.Msg;
@ -24,7 +25,22 @@ import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
class TargetActionTask extends Task {
public class TargetActionTask extends Task {
public static void executeTask(PluginTool tool, Task task) {
ProgressService progressService = tool.getService(ProgressService.class);
if (progressService != null) {
progressService.execute(task);
}
else {
tool.execute(task);
}
}
public static void runAction(PluginTool tool, String title, ActionEntry entry) {
executeTask(tool, new TargetActionTask(tool, title, entry));
}
private final PluginTool tool;
private final ActionEntry entry;

View file

@ -15,41 +15,51 @@
*/
package ghidra.app.plugin.core.debug.gui.control;
import java.util.Comparator;
import docking.ActionContext;
import docking.action.DockingAction;
import docking.action.KeyBindingType;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.target.Target.ActionEntry;
import ghidra.framework.plugintool.PluginTool;
class TargetDockingAction extends DockingAction {
private final DebuggerControlPlugin plugin;
private final PluginTool tool;
private final ActionName action;
private final String defaultDescription;
private ActionEntry entry;
public TargetDockingAction(String name, String owner, KeyBindingType keyBindingType,
DebuggerControlPlugin plugin, ActionName action, String defaultDescription) {
PluginTool tool, ActionName action, String defaultDescription) {
super(name, owner, keyBindingType);
this.plugin = plugin;
this.tool = tool;
this.action = action;
this.defaultDescription = defaultDescription;
}
private ActionEntry findEntry(ActionContext context) {
Target target = plugin.current.getTarget();
DebuggerTraceManagerService traceManager =
tool.getService(DebuggerTraceManagerService.class);
if (traceManager == null) {
return null;
}
Target target = traceManager.getCurrent().getTarget();
if (target == null) {
return null;
}
for (ActionEntry ent : target.collectActions(action, context).values()) {
if (ent.requiresPrompt()) {
continue;
}
return ent;
// TODO: What if multiple match? Do I care to display the extras?
}
return null;
return target.collectActions(action, context)
.values()
.stream()
.filter(e -> !e.requiresPrompt())
.sorted(Comparator.comparing(e -> -e.specificity()))
.findFirst()
.orElse(null);
// TODO: What if multiple match? Do I care to display the extras?
// Esp., step process vs step thread
}
protected void updateFromContext(ActionContext context) {
@ -73,6 +83,6 @@ class TargetDockingAction extends DockingAction {
if (entry == null) {
return;
}
plugin.runTask(getName(), entry);
TargetActionTask.runAction(tool, getName(), entry);
}
}

View file

@ -267,8 +267,8 @@ public class TraceRecorderTarget extends AbstractTarget {
private ActionEntry makeEntry(boolean requiresPrompt, TargetMethod method,
Map<String, ?> arguments) {
return new ActionEntry(method.getDisplay(), null, null, requiresPrompt, () -> true,
prompt -> invokeMethod(prompt, method, arguments));
return new ActionEntry(method.getDisplay(), null, null, requiresPrompt,
method.getPath().size(), () -> true, prompt -> invokeMethod(prompt, method, arguments));
}
@Override
@ -289,7 +289,7 @@ public class TraceRecorderTarget extends AbstractTarget {
return Map.of();
}
return Map.of(display, new ActionEntry(display, name, description, false,
() -> enabled.test(object), prompt -> action.apply(object)));
object.getPath().size(), () -> enabled.test(object), prompt -> action.apply(object)));
}
private TargetExecutionState getStateOf(TargetObject object) {
@ -358,6 +358,12 @@ public class TraceRecorderTarget extends AbstractTarget {
steppable -> steppable.step(TargetStepKind.EXTENDED));
}
@Override
protected Map<String, ActionEntry> collectRefreshActions(ActionContext context) {
// Not necessary to support this here
return Map.of();
}
@Override
public Trace getTrace() {
return recorder.getTrace();

View file

@ -197,7 +197,8 @@ public abstract class AbstractTarget implements Target {
collectStepIntoActions(context),
collectStepOverActions(context),
collectStepOutActions(context),
collectStepExtActions(context))
collectStepExtActions(context),
collectRefreshActions(context))
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
}
@ -216,6 +217,8 @@ public abstract class AbstractTarget implements Target {
protected abstract Map<String, ActionEntry> collectStepExtActions(ActionContext context);
protected abstract Map<String, ActionEntry> collectRefreshActions(ActionContext context);
@Override
public Map<String, ActionEntry> collectActions(ActionName name, ActionContext context) {
if (name == null) {
@ -245,6 +248,9 @@ public abstract class AbstractTarget implements Target {
else if (ActionName.STEP_EXT.equals(name)) {
return collectStepExtActions(context);
}
else if (ActionName.REFRESH.equals(name)) {
return collectRefreshActions(context);
}
Msg.warn(this, "Unrecognized action name: " + name);
return Map.of();
}

View file

@ -853,7 +853,7 @@ public class GdbMethodsTest extends AbstractGdbTraceRmiTest {
%s
start"""
.formatted(INSTRUMENT_STOPPED));
RemoteMethod stepAdvance = conn.getMethod("Advance");
RemoteMethod stepAdvance = conn.getMethod("step_advance");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
waitStopped();
@ -881,7 +881,7 @@ public class GdbMethodsTest extends AbstractGdbTraceRmiTest {
%s
start"""
.formatted(INSTRUMENT_STOPPED));
RemoteMethod stepReturn = conn.getMethod("Return");
RemoteMethod stepReturn = conn.getMethod("step_return");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/gdb/bash")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
waitStopped();

View file

@ -150,34 +150,34 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
}
protected void addControlMethods() {
rmiMethodResume = new TestRemoteMethod("resume", ActionName.RESUME,
rmiMethodResume = new TestRemoteMethod("resume", ActionName.RESUME, "Resume",
"Resume the target", EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process to resume"));
rmiMethodInterrupt = new TestRemoteMethod("interrupt", ActionName.INTERRUPT,
rmiMethodInterrupt = new TestRemoteMethod("interrupt", ActionName.INTERRUPT, "Interrupt",
"Interrupt the target", EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process to interrupt"));
rmiMethodKill = new TestRemoteMethod("kill", ActionName.KILL,
rmiMethodKill = new TestRemoteMethod("kill", ActionName.KILL, "Kill",
"Kill the target", EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process to kill"));
rmiMethodStepInto = new TestRemoteMethod("step_into", ActionName.STEP_INTO,
rmiMethodStepInto = new TestRemoteMethod("step_into", ActionName.STEP_INTO, "Step Into",
"Step the thread, descending into subroutines",
EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("thread", new SchemaName("Thread"), true, null, "Thread",
"The thread to step"));
rmiMethodStepOver = new TestRemoteMethod("step_over", ActionName.STEP_OVER,
rmiMethodStepOver = new TestRemoteMethod("step_over", ActionName.STEP_OVER, "Step Over",
"Step the thread, without descending into subroutines",
EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("thread", new SchemaName("Thread"), true, null, "Thread",
"The thread to step"));
rmiMethodStepOut = new TestRemoteMethod("step_out", ActionName.STEP_OUT,
rmiMethodStepOut = new TestRemoteMethod("step_out", ActionName.STEP_OUT, "Step Out",
"Allow the thread to finish the current subroutine",
EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("thread", new SchemaName("Thread"), true, null, "Thread",
@ -194,6 +194,7 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
protected void addBreakpointMethods() {
rmiMethodSetHwBreak = new TestRemoteMethod("set_hw_break", ActionName.BREAK_HW_EXECUTE,
"Hardware Breakpoint",
"Place a hardware execution breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process in which to place the breakpoint"),
@ -201,6 +202,7 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
null, "Address", "The desired address"));
rmiMethodSetSwBreak = new TestRemoteMethod("set_sw_break", ActionName.BREAK_SW_EXECUTE,
"Software Breakpoint",
"Place a software execution breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process in which to place the breakpoint"),
@ -208,6 +210,7 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
null, "Address", "The desired address"));
rmiMethodSetReadBreak = new TestRemoteMethod("set_read_break", ActionName.BREAK_READ,
"Read Breakpoint",
"Place a read breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process in which to place the breakpoint"),
@ -215,6 +218,7 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
null, "Range", "The desired address range"));
rmiMethodSetWriteBreak = new TestRemoteMethod("set_write_break", ActionName.BREAK_WRITE,
"Write Breakpoint",
"Place a write breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process in which to place the breakpoint"),
@ -222,6 +226,7 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
null, "Range", "The desired address range"));
rmiMethodSetAccessBreak = new TestRemoteMethod("set_acc_break", ActionName.BREAK_ACCESS,
"Access Breakpoint",
"Place an access breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process in which to place the breakpoint"),
@ -229,6 +234,7 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
null, "Range", "The desired address range"));
rmiMethodToggleBreak = new TestRemoteMethod("toggle_break", ActionName.TOGGLE,
"Toggle Breakpoint",
"Toggle a breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("breakpoint", new SchemaName("BreakpointSpec"), true, null,
"Breakpoint", "The breakpoint to toggle"),
@ -236,6 +242,7 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
null, "Enable", "True to enable. False to disable"));
rmiMethodDeleteBreak = new TestRemoteMethod("delete_break", ActionName.DELETE,
"Delete Breakpoint",
"Delete a breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("breakpoint", new SchemaName("BreakpointSpec"), true, null,
"Breakpoint", "The breakpoint to delete"));
@ -251,13 +258,13 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
}
protected void addRegisterMethods() {
rmiMethodReadRegs = new TestRemoteMethod("read_regs", ActionName.REFRESH,
rmiMethodReadRegs = new TestRemoteMethod("read_regs", ActionName.REFRESH, "Read Registers",
"Read registers", EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("container", new SchemaName("RegisterContainer"), true, null,
"Registers", "The registers node to read"));
rmiMethodWriteReg = new TestRemoteMethod("write_reg", ActionName.WRITE_REG,
"Write a register", EnumerableTargetObjectSchema.VOID.getName(),
"Write Register", "Write a register", EnumerableTargetObjectSchema.VOID.getName(),
new TestRemoteParameter("frame", new SchemaName("Frame"), false, 0, "Frame",
"The frame to write to"),
new TestRemoteParameter("name", EnumerableTargetObjectSchema.STRING.getName(), true,