GP-3857: Port most Debugger components to TraceRmi.

This commit is contained in:
Dan
2023-11-02 10:43:31 -04:00
parent 7e4d2bcfaa
commit fd4380c07a
222 changed files with 7241 additions and 3752 deletions
@@ -46,7 +46,10 @@ shift
target_args="$@"
"$OPT_GDB_PATH" \
-q \
-ex "set pagination off" \
-ex "set confirm off" \
-ex "show version" \
-ex "python import ghidragdb" \
-ex "file \"$target_image\"" \
-ex "set args $target_args" \
@@ -55,4 +58,5 @@ target_args="$@"
-ex "ghidra trace start" \
-ex "ghidra trace sync-enable" \
-ex "$OPT_START_CMD" \
-ex "set confirm on" \
-ex "set pagination on"
@@ -239,11 +239,18 @@ class DefaultRegisterMapper(object):
.format(name, value, value.type))
return RegVal(self.map_name(inf, name), av)
def convert_value_back(self, value, size=None):
if size is not None:
value = value[-size:].rjust(size, b'\0')
if self.byte_order == 'little':
value = bytes(reversed(value))
return value
def map_name_back(self, inf, name):
return name
def map_value_back(self, inf, name, value):
return RegVal(self.map_name_back(inf, name), value)
return RegVal(self.map_name_back(inf, name), self.convert_value_back(value))
class Intel_x86_64_RegisterMapper(DefaultRegisterMapper):
@@ -268,6 +275,7 @@ class Intel_x86_64_RegisterMapper(DefaultRegisterMapper):
def map_name_back(self, inf, name):
if name == 'rflags':
return 'eflags'
return name
DEFAULT_BE_REGISTER_MAPPER = DefaultRegisterMapper('big')
@@ -50,6 +50,8 @@ STACK_PATTERN = THREAD_PATTERN + '.Stack'
FRAME_KEY_PATTERN = '[{level}]'
FRAME_PATTERN = STACK_PATTERN + FRAME_KEY_PATTERN
REGS_PATTERN = FRAME_PATTERN + '.Registers'
REG_KEY_PATTERN = '[{regname}]'
REG_PATTERN = REGS_PATTERN + REG_KEY_PATTERN
MEMORY_PATTERN = INFERIOR_PATTERN + '.Memory'
REGION_KEY_PATTERN = '[{start:08x}]'
REGION_PATTERN = MEMORY_PATTERN + REGION_KEY_PATTERN
@@ -564,15 +566,26 @@ def putreg(frame, reg_descs):
space = REGS_PATTERN.format(infnum=inf.num, tnum=gdb.selected_thread().num,
level=frame.level())
STATE.trace.create_overlay_space('register', space)
robj = STATE.trace.create_object(space)
robj.insert()
cobj = STATE.trace.create_object(space)
cobj.insert()
mapper = STATE.trace.register_mapper
keys = []
values = []
for desc in reg_descs:
v = frame.read_register(desc)
values.append(mapper.map_value(inf, desc.name, v))
rv = mapper.map_value(inf, desc.name, v)
values.append(rv)
# TODO: Key by gdb's name or mapped name? I think gdb's.
rpath = REG_PATTERN.format(infnum=inf.num, tnum=gdb.selected_thread(
).num, level=frame.level(), regname=desc.name)
keys.append(REG_KEY_PATTERN.format(regname=desc.name))
robj = STATE.trace.create_object(rpath)
robj.set_value('_value', rv.value)
robj.insert()
cobj.retain_values(keys)
# TODO: Memorize registers that failed for this arch, and omit later.
return {'missing': STATE.trace.put_registers(space, values)}
missing = STATE.trace.put_registers(space, values)
return {'missing': missing}
@cmd('ghidra trace putreg', '-ghidra-trace-putreg', gdb.COMMAND_DATA, True)
@@ -585,7 +598,8 @@ def ghidra_trace_putreg(group='all', *, is_mi, **kwargs):
STATE.require_tx()
frame = gdb.selected_frame()
return putreg(frame, frame.architecture().registers(group))
with STATE.client.batch() as b:
return putreg(frame, frame.architecture().registers(group))
@cmd('ghidra trace delreg', '-ghidra-trace-delreg', gdb.COMMAND_DATA, True)
@@ -977,6 +991,17 @@ def compute_inf_state(inf):
return 'STOPPED'
def put_inferior_state(inf):
ipath = INFERIOR_PATTERN.format(infnum=inf.num)
infobj = STATE.trace.proxy_object_path(ipath)
istate = compute_inf_state(inf)
infobj.set_value('_state', istate)
for t in inf.threads():
tpath = THREAD_PATTERN.format(infnum=inf.num, tnum=t.num)
tobj = STATE.trace.proxy_object_path(tpath)
tobj.set_value('_state', convert_state(t))
def put_inferiors():
# TODO: Attributes like _exit_code, _state?
# _state would be derived from threads
@@ -1034,6 +1059,7 @@ def put_single_breakpoint(b, ibobj, inf, ikeys):
mapper = STATE.trace.memory_mapper
bpath = BREAKPOINT_PATTERN.format(breaknum=b.number)
brkobj = STATE.trace.create_object(bpath)
brkobj.set_value('_enabled', b.enabled)
if b.type == gdb.BP_BREAKPOINT:
brkobj.set_value('_expression', b.location)
brkobj.set_value('_kinds', 'SW_EXECUTE')
@@ -1073,6 +1099,7 @@ def put_single_breakpoint(b, ibobj, inf, ikeys):
if inf.num not in l.thread_groups:
continue
locobj = STATE.trace.create_object(bpath + k)
locobj.set_value('_enabled', l.enabled)
ik = INF_BREAK_KEY_PATTERN.format(breaknum=b.number, locnum=i+1)
ikeys.append(ik)
if b.location is not None: # Implies execution break
@@ -31,12 +31,13 @@ GhidraHookPrefix()
class HookState(object):
__slots__ = ('installed', 'mem_catchpoint', 'batch')
__slots__ = ('installed', 'mem_catchpoint', 'batch', 'skip_continue')
def __init__(self):
self.installed = False
self.mem_catchpoint = None
self.batch = None
self.skip_continue = False
def ensure_batch(self):
if self.batch is None:
@@ -48,6 +49,11 @@ class HookState(object):
commands.STATE.client.end_batch()
self.batch = None
def check_skip_continue(self):
skip = self.skip_continue
self.skip_continue = False
return skip
class InferiorState(object):
__slots__ = ('first', 'regions', 'modules', 'threads', 'breaks', 'visited')
@@ -70,6 +76,8 @@ class InferiorState(object):
if first:
commands.put_inferiors()
commands.put_environment()
else:
commands.put_inferior_state(gdb.selected_inferior())
if self.threads:
commands.put_threads()
self.threads = False
@@ -81,7 +89,8 @@ class InferiorState(object):
frame = gdb.selected_frame()
hashable_frame = (thread, frame.level())
if first or hashable_frame not in self.visited:
commands.putreg(frame, frame.architecture().registers())
commands.putreg(
frame, frame.architecture().registers('general'))
commands.putmem("$pc", "1", from_tty=False)
commands.putmem("$sp", "1", from_tty=False)
self.visited.add(hashable_frame)
@@ -224,7 +233,6 @@ def on_memory_changed(event):
def on_register_changed(event):
gdb.write("Register changed: {}".format(dir(event)))
inf = gdb.selected_inferior()
if inf.num not in INF_STATES:
return
@@ -240,6 +248,8 @@ def on_register_changed(event):
def on_cont(event):
if (HOOK_STATE.check_skip_continue()):
return
inf = gdb.selected_inferior()
if inf.num not in INF_STATES:
return
@@ -254,6 +264,7 @@ def on_cont(event):
def on_stop(event):
if hasattr(event, 'breakpoints') and HOOK_STATE.mem_catchpoint in event.breakpoints:
HOOK_STATE.skip_continue = True
return
inf = gdb.selected_inferior()
if inf.num not in INF_STATES:
@@ -337,6 +348,8 @@ def on_breakpoint_created(b):
def on_breakpoint_modified(b):
if b == HOOK_STATE.mem_catchpoint:
return
inf = gdb.selected_inferior()
notify_others_breaks(inf)
if inf.num not in INF_STATES:
@@ -438,6 +451,16 @@ def hook_frame():
on_frame_selected()
@cmd_hook('hookpost-up')
def hook_frame_up():
on_frame_selected()
@cmd_hook('hookpost-down')
def hook_frame_down():
on_frame_selected()
# TODO: Checks and workarounds for events missing in gdb 8
def install_hooks():
if HOOK_STATE.installed:
@@ -451,6 +474,8 @@ def install_hooks():
gdb.events.new_thread.connect(on_new_thread)
hook_thread.hook()
hook_frame.hook()
hook_frame_up.hook()
hook_frame_down.hook()
# Respond to user-driven state changes: (Not target-driven)
gdb.events.memory_changed.connect(on_memory_changed)
@@ -508,6 +533,8 @@ def remove_hooks():
gdb.events.new_thread.disconnect(on_new_thread)
hook_thread.unhook()
hook_frame.unhook()
hook_frame_up.unhook()
hook_frame_down.unhook()
gdb.events.memory_changed.disconnect(on_memory_changed)
gdb.events.register_changed.disconnect(on_register_changed)
@@ -14,6 +14,7 @@
# limitations under the License.
##
from concurrent.futures import Future, Executor
from contextlib import contextmanager
import re
from ghidratrace import sch
@@ -24,13 +25,30 @@ import gdb
from . import commands, hooks, util
@contextmanager
def no_pagination():
before = gdb.parameter('pagination')
gdb.set_parameter('pagination', False)
yield
gdb.set_parameter('pagination', before)
@contextmanager
def no_confirm():
before = gdb.parameter('confirm')
gdb.set_parameter('confirm', False)
yield
gdb.set_parameter('confirm', before)
class GdbExecutor(Executor):
def submit(self, fn, *args, **kwargs):
fut = Future()
def _exec():
try:
result = fn(*args, **kwargs)
with no_pagination():
result = fn(*args, **kwargs)
hooks.HOOK_STATE.end_batch()
fut.set_result(result)
except Exception as e:
@@ -186,7 +204,9 @@ def find_frame_by_regs_obj(object):
# Because there's no method to get a register by name....
def find_reg_by_name(f, name):
for reg in f.architecture().registers():
if reg.name == name:
# TODO: gdb appears to be case sensitive, but until we encounter a
# situation where case matters, we'll be insensitive
if reg.name.lower() == name.lower():
return reg
raise KeyError(f"No such register: {name}")
@@ -453,7 +473,8 @@ def launch_run(inferior: sch.Schema('Inferior'),
def kill(inferior: sch.Schema('Inferior')):
"""Kill execution of the inferior."""
switch_inferior(find_inf_by_obj(inferior))
gdb.execute('kill')
with no_confirm():
gdb.execute('kill')
@REGISTRY.method
@@ -463,8 +484,11 @@ def resume(inferior: sch.Schema('Inferior')):
gdb.execute('continue')
# Technically, inferior is not required, but it hints that the affected object
# is the current inferior. This in turn queues the UI to enable or disable the
# button appropriately
@REGISTRY.method
def interrupt():
def interrupt(inferior: sch.Schema('Inferior')):
"""Interrupt the execution of the debugged program."""
gdb.execute('interrupt')
@@ -490,7 +514,7 @@ def step_out(thread: sch.Schema('Thread')):
gdb.execute('finish')
@REGISTRY.method(action='step_ext')
@REGISTRY.method(action='step_ext', name='Advance')
def step_advance(thread: sch.Schema('Thread'), address: Address):
"""Continue execution up to the given address (advance)."""
t = find_thread_by_obj(thread)
@@ -499,7 +523,7 @@ def step_advance(thread: sch.Schema('Thread'), address: Address):
gdb.execute(f'advance *0x{offset:x}')
@REGISTRY.method(action='step_ext')
@REGISTRY.method(action='step_ext', name='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()
@@ -641,13 +665,13 @@ def write_mem(inferior: sch.Schema('Inferior'), address: Address, data: bytes):
@REGISTRY.method
def write_reg(frame: sch.Schema('Frame'), name: str, value: bytes):
def write_reg(frame: sch.Schema('StackFrame'), name: str, value: bytes):
"""Write a register."""
f = find_frame_by_obj(frame)
f.select()
inf = gdb.selected_inferior()
mname, mval = frame.trace.register_mapper.map_value_back(inf, name, value)
reg = find_reg_by_name(f, mname)
size = int(gdb.parse_and_eval(f'sizeof(${mname})'))
size = int(gdb.parse_and_eval(f'sizeof(${reg.name})'))
arr = '{' + ','.join(str(b) for b in mval) + '}'
gdb.execute(f'set ((unsigned char[{size}])${mname}) = {arr}')
gdb.execute(f'set ((unsigned char[{size}])${reg.name}) = {arr}')
@@ -18,7 +18,6 @@ package ghidra.app.services;
import java.util.Collection;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.debug.api.tracermi.TraceRmiLaunchOpinion;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.program.model.listing.Program;
@@ -29,13 +28,6 @@ import ghidra.program.model.listing.Program;
description = "Manages and presents launchers for Trace RMI Targets",
defaultProviderName = "ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLauncherServicePlugin")
public interface TraceRmiLauncherService {
/**
* Get all of the installed opinions
*
* @return the opinions
*/
Collection<TraceRmiLaunchOpinion> getOpinions();
/**
* Get all offers for the given program
*
@@ -87,4 +87,14 @@ public interface LocationTracker {
* @return true if re-computation and "goto" is warranted
*/
boolean affectedByStackChange(TraceStack stack, DebuggerCoordinates coordinates);
/**
* Indicates whether the user should expect instructions at the tracked location.
*
* <p>
* Essentially, is this tracking the program counter?
*
* @return true to disassemble, false not to
*/
boolean shouldDisassemble();
}
@@ -15,6 +15,9 @@
*/
package ghidra.debug.api.target;
import java.util.HashMap;
import java.util.Map;
/**
* A name for a commonly-recognized target action.
*
@@ -31,15 +34,43 @@ package ghidra.debug.api.target;
* 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.
*/
public record ActionName(String name) {
public static final ActionName REFRESH = new ActionName("refresh");
public record ActionName(String name, boolean builtIn) {
private static final Map<String, ActionName> NAMES = new HashMap<>();
public static ActionName name(String name) {
synchronized (NAMES) {
return NAMES.computeIfAbsent(name, n -> new ActionName(n, false));
}
}
private static ActionName builtIn(String name) {
synchronized (NAMES) {
ActionName action = new ActionName(name, true);
if (NAMES.put(name, action) != null) {
throw new AssertionError();
}
return action;
}
}
private static ActionName extended(String name) {
synchronized (NAMES) {
ActionName action = new ActionName(name, false);
if (NAMES.put(name, action) != null) {
throw new AssertionError();
}
return action;
}
}
public static final ActionName REFRESH = builtIn("refresh");
/**
* Activate a given object and optionally a time
*
* <p>
* Forms: (focus:Object), (focus:Object, snap:LONG), (focus:Object, time:STR)
*/
public static final ActionName ACTIVATE = new ActionName("activate");
public static final ActionName ACTIVATE = builtIn("activate");
/**
* A weaker form of activate.
*
@@ -48,9 +79,9 @@ public record ActionName(String name) {
* used to communicate selection (i.e., highlight) of the object. Whereas, double-clicking or
* pressing enter would more likely invoke 'activate.'
*/
public static final ActionName FOCUS = new ActionName("focus");
public static final ActionName TOGGLE = new ActionName("toggle");
public static final ActionName DELETE = new ActionName("delete");
public static final ActionName FOCUS = builtIn("focus");
public static final ActionName TOGGLE = builtIn("toggle");
public static final ActionName DELETE = builtIn("delete");
/**
* Execute a CLI command
@@ -58,7 +89,7 @@ public record ActionName(String name) {
* <p>
* Forms: (cmd:STRING):STRING; Optional arguments: capture:BOOL
*/
public static final ActionName EXECUTE = new ActionName("execute");
public static final ActionName EXECUTE = builtIn("execute");
/**
* Connect the back-end to a (usually remote) target
@@ -66,23 +97,23 @@ public record ActionName(String name) {
* <p>
* Forms: (spec:STRING)
*/
public static final ActionName CONNECT = new ActionName("connect");
public static final ActionName CONNECT = extended("connect");
/**
* Forms: (target:Attachable), (pid:INT), (spec:STRING)
*/
public static final ActionName ATTACH = new ActionName("attach");
public static final ActionName DETACH = new ActionName("detach");
public static final ActionName ATTACH = extended("attach");
public static final ActionName DETACH = extended("detach");
/**
* Forms: (command_line:STRING), (file:STRING,args:STRING), (file:STRING,args:STRING_ARRAY),
* (ANY*)
*/
public static final ActionName LAUNCH = new ActionName("launch");
public static final ActionName KILL = new ActionName("kill");
public static final ActionName LAUNCH = extended("launch");
public static final ActionName KILL = builtIn("kill");
public static final ActionName RESUME = new ActionName("resume");
public static final ActionName INTERRUPT = new ActionName("interrupt");
public static final ActionName RESUME = builtIn("resume");
public static final ActionName INTERRUPT = builtIn("interrupt");
/**
* All of these will show in the "step" portion of the control toolbar, if present. The
@@ -93,25 +124,25 @@ public record ActionName(String name) {
* context. (Multiple will appear, but may confuse the user.) You can have as many extended step
* actions as you like. They will be ordered lexicographically by name.
*/
public static final ActionName STEP_INTO = new ActionName("step_into");
public static final ActionName STEP_OVER = new ActionName("step_over");
public static final ActionName STEP_OUT = new ActionName("step_out");
public static final ActionName STEP_INTO = builtIn("step_into");
public static final ActionName STEP_OVER = builtIn("step_over");
public static final ActionName STEP_OUT = builtIn("step_out");
/**
* Skip is not typically available, except in emulators. If the back-end debugger does not have
* a command for this action out-of-the-box, we do not recommend trying to implement it
* yourself. The purpose of these actions just to expose/map each command to the UI, not to
* invent new features for the back-end debugger.
*/
public static final ActionName STEP_SKIP = new ActionName("step_skip");
public static final ActionName STEP_SKIP = builtIn("step_skip");
/**
* Step back is not typically available, except in emulators and timeless (or time-travel)
* debuggers.
*/
public static final ActionName STEP_BACK = new ActionName("step_back");
public static final ActionName STEP_BACK = builtIn("step_back");
/**
* The action for steps that don't fit one of the common stepping actions.
*/
public static final ActionName STEP_EXT = new ActionName("step_ext");
public static final ActionName STEP_EXT = extended("step_ext");
/**
* Forms: (addr:ADDRESS), R/W(rng:RANGE), (expr:STRING)
@@ -123,25 +154,25 @@ public record ActionName(String name) {
* The client may pass either null or "" for condition and/or commands to indicate omissions of
* those arguments.
*/
public static final ActionName BREAK_SW_EXECUTE = new ActionName("break_sw_execute");
public static final ActionName BREAK_HW_EXECUTE = new ActionName("break_hw_execute");
public static final ActionName BREAK_READ = new ActionName("break_read");
public static final ActionName BREAK_WRITE = new ActionName("break_write");
public static final ActionName BREAK_ACCESS = new ActionName("break_access");
public static final ActionName BREAK_EXT = new ActionName("break_ext");
public static final ActionName BREAK_SW_EXECUTE = builtIn("break_sw_execute");
public static final ActionName BREAK_HW_EXECUTE = builtIn("break_hw_execute");
public static final ActionName BREAK_READ = builtIn("break_read");
public static final ActionName BREAK_WRITE = builtIn("break_write");
public static final ActionName BREAK_ACCESS = builtIn("break_access");
public static final ActionName BREAK_EXT = extended("break_ext");
/**
* Forms: (rng:RANGE)
*/
public static final ActionName READ_MEM = new ActionName("read_mem");
public static final ActionName READ_MEM = builtIn("read_mem");
/**
* Forms: (addr:ADDRESS,data:BYTES)
*/
public static final ActionName WRITE_MEM = new ActionName("write_mem");
public static final ActionName WRITE_MEM = builtIn("write_mem");
// NOTE: no read_reg. Use refresh(RegContainer), refresh(RegGroup), refresh(Register)
/**
* Forms: (frame:Frame,name:STRING,value:BYTES), (register:Register,value:BYTES)
*/
public static final ActionName WRITE_REG = new ActionName("write_reg");
public static final ActionName WRITE_REG = builtIn("write_reg");
}
@@ -19,14 +19,11 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.util.function.Function;
import docking.ActionContext;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.Register;
import ghidra.program.model.lang.RegisterValue;
@@ -38,39 +35,81 @@ import ghidra.trace.model.memory.TraceMemoryState;
import ghidra.trace.model.stack.TraceStackFrame;
import ghidra.trace.model.target.TraceObjectKeyPath;
import ghidra.trace.model.thread.TraceThread;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* The interface between the front-end UI and the back-end connector.
*
* <p>
* Anything the UI might command a target to do must be defined as a method here. Each
* implementation can then sort out, using context from the UI as appropriate, how best to effect
* the command using the protocol and resources available on the back-end.
*/
public interface Target {
long TIMEOUT_MILLIS = 10000;
/**
* A description of a UI action provided by this target.
*
* <p>
* In most cases, this will generate a menu entry or a toolbar button, but in some cases, it's
* just invoked implicitly. Often, the two suppliers are implemented using lambda functions, and
* those functions will keep whatever some means of querying UI and/or target context in their
* closures.
*
* @param display the text to display on UI actions associated with this entry
* @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 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, Supplier<CompletableFuture<?>> action) {
BooleanSupplier enabled, Function<Boolean, CompletableFuture<?>> action) {
/**
* Check if this action is currently enabled
*
* @return true if enabled
*/
public boolean isEnabled() {
return enabled.getAsBoolean();
}
/**
* Invoke the action asynchronously, prompting if desired
*
* <p>
* Note this will impose a timeout of {@value Target#TIMEOUT_MILLIS} milliseconds.
*
* @param prompt whether or not to prompt the user for arguments
* @return the future result, often {@link Void}
*/
public CompletableFuture<?> invokeAsync(boolean prompt) {
return action.get().orTimeout(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
public CompletableFuture<?> invokeAsyncLogged(boolean prompt, PluginTool tool) {
return invokeAsync(prompt).exceptionally(ex -> {
if (tool != null) {
tool.setStatusInfo(display + " failed: " + ex, true);
}
Msg.error(this, display + " failed: " + ex, ex);
return ExceptionUtils.rethrow(ex);
});
return action.apply(prompt).orTimeout(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
/**
* Invoke the action synchronously
*
* <p>
* To avoid blocking the Swing thread on a remote socket, this method cannot be called on
* the Swing thread.
*
* @param prompt whether or not to prompt the user for arguments
*/
public void run(boolean prompt) {
get(prompt);
}
/**
* Invoke the action synchronously, getting its result
*
* @param prompt whether or not to prompt the user for arguments
* @return the resulting value, if applicable
*/
public Object get(boolean prompt) {
if (Swing.isSwingThread()) {
throw new AssertionError("Refusing to block the Swing thread. Use a Task.");
@@ -82,30 +121,107 @@ public interface Target {
throw new RuntimeException(e);
}
}
/**
* Check if this action's name is built in
*
* @return true if built in.
*/
public boolean builtIn() {
return name != null && name.builtIn();
}
}
/**
* Check if the target is still valid
*
* @return true if valid
*/
boolean isValid();
/**
* Get the trace into which this target is recorded
*
* @return the trace
*/
Trace getTrace();
/**
* Get the current snapshot key for the target
*
* <p>
* For most targets, this is the most recently created snapshot.
*
* @return the snapshot
*/
// TODO: Should this be TraceSchedule getTime()?
long getSnap();
/**
* Collect all actions that implement the given common debugger command
*
* @param name the action name
* @param context applicable context from the UI
* @return the collected actions
*/
Map<String, ActionEntry> collectActions(ActionName name, ActionContext context);
/**
* Get the trace thread that contains the given object
*
* @param path the path of the object
* @return the thread, or null
*/
TraceThread getThreadForSuccessor(TraceObjectKeyPath path);
/**
* Get the execution state of the given thread
*
* @param thread the thread
* @return the state
*/
TargetExecutionState getThreadExecutionState(TraceThread thread);
/**
* Get the trace stack frame that contains the given object
*
* @param path the path of the object
* @return the stack frame, or null
*/
TraceStackFrame getStackFrameForSuccessor(TraceObjectKeyPath path);
/**
* Check if the target supports synchronizing focus
*
* @return true if supported
*/
boolean isSupportsFocus();
/**
* Get the object that currently has focus on the back end's UI
*
* @return the focused object's path, or null
*/
TraceObjectKeyPath getFocus();
/**
* @see #activate(DebuggerCoordinates, DebuggerCoordinates)
*/
CompletableFuture<Void> activateAsync(DebuggerCoordinates prev, DebuggerCoordinates coords);
/**
* Request that the back end's focus be set to the same as the front end's (Ghidra's) GUI.
*
* @param prev the GUI's immediately previous coordinates
* @param coords the GUI's current coordinates
*/
void activate(DebuggerCoordinates prev, DebuggerCoordinates coords);
/**
* @see #invalidateMemoryCaches()
*/
CompletableFuture<Void> invalidateMemoryCachesAsync();
/**
* Invalidate any caches on the target's back end or on the client side of the connection.
*
@@ -118,11 +234,6 @@ public interface Target {
* <b>NOTE:</b> This method exists for invalidating model-based target caches. It may be
* deprecated and removed, unless it turns out we need this for Trace RMI, too.
*/
CompletableFuture<Void> invalidateMemoryCachesAsync();
/**
* See {@link #invalidateMemoryCachesAsync()}
*/
void invalidateMemoryCaches();
/**
@@ -135,11 +246,11 @@ public interface Target {
*
* <p>
* The target may read more than the requested memory, usually because it will read all pages
* containing any portion of the requested set.
*
* <p>
* This task is relatively error tolerant. If a range cannot be captured -- a common occurrence
* -- the error is logged without throwing an exception.
* containing any portion of the requested set. The target should attempt to read at least the
* given memory. To the extent it is successful, it must cause the values to be recorded into
* the trace <em>before</em> this method returns. Only if the request is <em>entirely</em>
* unsuccessful should this method throw an exception. Otherwise, the failed portions, if any,
* should be logged without throwing an exception.
*
* @param set the addresses to capture
* @param monitor a monitor for displaying task steps
@@ -147,30 +258,97 @@ public interface Target {
*/
void readMemory(AddressSetView set, TaskMonitor monitor) throws CancelledException;
/**
* @see #readMemory(AddressSetView, TaskMonitor)
*/
CompletableFuture<Void> writeMemoryAsync(Address address, byte[] data);
/**
* Write data to the target's memory
*
* <p>
* The target should attempt to write the memory. To the extent it is successful, it must cause
* the effects to be recorded into the trace <em>before</em> this method returns. Only if the
* request is <em>entirely</em> unsuccessful should this method throw an exception. Otherwise,
* the failed portions, if any, should be logged without throwing an exception.
*
* @param address the starting address
* @param data the bytes to write
*/
void writeMemory(Address address, byte[] data);
/**
* @see #readRegisters(TracePlatform, TraceThread, int, Set)
*/
CompletableFuture<Void> readRegistersAsync(TracePlatform platform, TraceThread thread,
int frame, Set<Register> registers);
/**
* Read and capture the named target registers for the given platform, thread, and frame.
*
* <p>
* Target target should read the registers and, to the extent it is successful, cause the values
* to be recorded into the trace <em>before</em> this method returns. Only if the request is
* <em>entirely</em> unsuccessful should this method throw an exception. Otherwise, the failed
* registers, if any, should be logged without throwing an exception.
*
* @param platform the platform defining the registers
* @param thread the thread whose context contains the register values
* @param frame the frame, if applicable, for saved register values. 0 for current values.
* @param registers the registers to read
*/
void readRegisters(TracePlatform platform, TraceThread thread, int frame,
Set<Register> registers);
/**
* @see #readRegistersAsync(TracePlatform, TraceThread, int, AddressSetView)
*/
CompletableFuture<Void> readRegistersAsync(TracePlatform platform, TraceThread thread,
int frame, AddressSetView guestSet);
/**
* Read and capture the target registers in the given address set.
*
* <p>
* Aside from how registers are named, this works equivalently to
* {@link #readRegisters(TracePlatform, TraceThread, int, Set)}.
*/
void readRegisters(TracePlatform platform, TraceThread thread, int frame,
AddressSetView guestSet);
/**
* @see #writeRegister(TracePlatform, TraceThread, int, RegisterValue)
*/
CompletableFuture<Void> writeRegisterAsync(TracePlatform platform, TraceThread thread,
int frame, RegisterValue value);
/**
* Write a value to a target register for the given platform, thread, and frame
*
* <p>
* The target should attempt to write the register. If successful, it must cause the effects to
* be recorded into the trace <em>before</em> this method returns. If the request is
* unsuccessful, this method throw an exception.
*
* @param address the starting address
* @param data the bytes to write
*/
void writeRegister(TracePlatform platform, TraceThread thread, int frame, RegisterValue value);
/**
* @see #writeRegister(TracePlatform, TraceThread, int, Address, byte[])
*/
CompletableFuture<Void> writeRegisterAsync(TracePlatform platform, TraceThread thread,
int frame, Address address, byte[] data);
/**
* Write a value to a target register by its address
*
* <p>
* Aside from how the register is named, this works equivalently to
* {@link #writeRegister(TracePlatform, TraceThread, int, RegisterValue)}. The address is the
* one defined by Ghidra.
*/
void writeRegister(TracePlatform platform, TraceThread thread, int frame, Address address,
byte[] data);
@@ -179,7 +357,7 @@ public interface Target {
*
* @param platform the platform whose language defines the registers
* @param thread if a register, the thread whose registers to examine
* @param frameLevel the frame, usually 0.
* @param frame the frame level, usually 0.
* @param address the address of the variable
* @param size the size of the variable. Ignored for memory
* @return true if the variable can be mapped to the target
@@ -211,11 +389,35 @@ public interface Target {
void writeVariable(TracePlatform platform, TraceThread thread, int frame, Address address,
byte[] data);
/**
* Get the kinds of breakpoints supported by the target.
*
* @return the set of kinds
*/
Set<TraceBreakpointKind> getSupportedBreakpointKinds();
/**
* @see #placeBreakpoint(AddressRange, Set, String, String)
*/
CompletableFuture<Void> placeBreakpointAsync(AddressRange range,
Set<TraceBreakpointKind> kinds, String condition, String commands);
/**
* Place a new breakpoint of the given kind(s) over the given range
*
* <p>
* If successful, this method must cause the breakpoint to be recorded into the trace.
* Otherwise, it should throw an exception.
*
* @param range the range. NOTE: The target is only required to support length-1 execution
* breakpoints.
* @param kinds the kind(s) of the breakpoint.
* @param condition optionally, a condition for the breakpoint, expressed in the back-end's
* language. NOTE: May be silently ignored by the implementation, if not supported.
* @param commands optionally, a command to execute upon hitting the breakpoint, expressed in
* the back-end's language. NOTE: May be silently ignored by the implementation, if
* not supported.
*/
void placeBreakpoint(AddressRange range, Set<TraceBreakpointKind> kinds, String condition,
String commands);
@@ -227,14 +429,61 @@ public interface Target {
*/
boolean isBreakpointValid(TraceBreakpoint breakpoint);
/**
* @see #deleteBreakpoint(TraceBreakpoint)
*/
CompletableFuture<Void> deleteBreakpointAsync(TraceBreakpoint breakpoint);
/**
* Delete the given breakpoint from the target
*
* <p>
* If successful, this method must cause the breakpoint removal to be recorded in the trace.
* Otherwise, it should throw an exception.
*
* @param breakpoint the breakpoint to delete
*/
void deleteBreakpoint(TraceBreakpoint breakpoint);
/**
* @see #toggleBreakpoint(TraceBreakpoint, boolean)
*/
CompletableFuture<Void> toggleBreakpointAsync(TraceBreakpoint breakpoint, boolean enabled);
/**
* Toggle the given breakpoint on the target
*
* <p>
* If successful, this method must cause the breakpoint toggle to be recorded in the trace. If
* the state is already as desired, this method may have no effect. If unsuccessful, this method
* should throw an exception.
*
* @param breakpoint the breakpoint to toggle
* @param enabled true to enable, false to disable
*/
void toggleBreakpoint(TraceBreakpoint breakpoint, boolean enabled);
/**
* @see #forceTerminate()
*/
CompletableFuture<Void> forceTerminateAsync();
/**
* Forcefully terminate the target
*
* <p>
* This will first attempt to kill the target gracefully. In addition, and whether or not the
* target is successfully terminated, the target will be dissociated from its trace, and the
* target will be invalidated. To attempt only a graceful termination, check
* {@link #collectActions(ActionName, ActionContext)} with {@link ActionName#KILL}.
*/
void forceTerminate();
/**
* @see #disconnect()
*/
CompletableFuture<Void> disconnectAsync();
/**
* Terminate the target and its connection
*
@@ -244,13 +493,6 @@ public interface Target {
* the debugger is configured to remain attached to both. Whether this is expected or acceptable
* behavior has not been decided.
*
* @see #disconnect()
*/
CompletableFuture<Void> disconnectAsync();
/**
* Terminate the target and its connection
*
* <p>
* <b>NOTE:</b> This method cannot be invoked on the Swing thread, because it may block on I/O.
*
@@ -15,8 +15,21 @@
*/
package ghidra.debug.api.target;
/**
* A listener for changes to the set of published targets
*/
public interface TargetPublicationListener {
/**
* The given target was published
*
* @param target the published target
*/
void targetPublished(Target target);
/**
* The given target was withdrawn, usually because it's no longer valid
*
* @param target the withdrawn target
*/
void targetWithdrawn(Target target);
}
@@ -658,13 +658,12 @@ public class DebuggerCoordinates {
projData = new DefaultProjectData(projLoc, false, false);
}
catch (NotOwnerException e) {
Msg.showError(DebuggerCoordinates.class, tool.getToolFrame(), "Trace Open Failed",
Msg.error(DebuggerCoordinates.class,
"Not project owner: " + projLoc + "(" + pathname + ")");
return null;
}
catch (IOException | LockException e) {
Msg.showError(DebuggerCoordinates.class, tool.getToolFrame(), "Trace Open Failed",
"Project error: " + e.getMessage());
Msg.error(DebuggerCoordinates.class, "Project error: " + e.getMessage());
return null;
}
}
@@ -676,8 +675,7 @@ public class DebuggerCoordinates {
if (version != DomainFile.DEFAULT_VERSION) {
message += " version " + version;
}
String title = df == null ? "Trace Not Found" : "Wrong File Type";
Msg.showError(DebuggerCoordinates.class, tool.getToolFrame(), title, message);
Msg.error(DebuggerCoordinates.class, message);
return null;
}
return df;
@@ -13,21 +13,33 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.tracermi;
import java.util.Collection;
import java.io.IOException;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.ServiceInfo;
/**
* A terminal with some back-end element attached to it
*/
public interface TerminalSession extends AutoCloseable {
@Override
void close() throws IOException;
@ServiceInfo(
defaultProviderName = "ghidra.app.plugin.core.debug.service.workflow.DebuggerWorkflowServicePlugin",
description = "Service for managing automatic debugger actions and analysis")
public interface DebuggerWorkflowFrontEndService extends DebuggerWorkflowService {
/**
* Get all the tools with the corresponding {@link DebuggerWorkflowToolService}
*
* @return the tools proxying this service
* Terminate the session without closing the terminal
*/
Collection<PluginTool> getProxyingPluginTools();
void terminate() throws IOException;
/**
* Check whether the terminal session is terminated or still active
*
* @return true for terminated, false for active
*/
boolean isTerminated();
/**
* Provide a human-readable description of the session
*
* @return the description
*/
String description();
}
@@ -17,23 +17,124 @@ package ghidra.debug.api.tracermi;
import java.io.IOException;
import java.net.SocketAddress;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeoutException;
import ghidra.trace.model.Trace;
/**
* A connection to a TraceRmi back end
*
* <p>
* TraceRmi is a two-way request-reply channel, usually over TCP. The back end, i.e., the trace-rmi
* plugin hosted in the target platform's actual debugger, is granted a fixed set of
* methods/messages for creating and populating a {@link Trace}. Each such trace is designated as a
* target. The back end provides a set of methods for the front-end to use to control the connection
* and its targets. For a given connection, the methods are fixed, but each back end may provide a
* different set of methods to best describe/model its command set. The same methods are applicable
* to all of the back end's target. While uncommon, one back end may create several targets. E.g.,
* if a target creates a child process, and the back-end debugger is configured to remain attached
* to both parent and child, then it should create and publish a second target.
*/
public interface TraceRmiConnection extends AutoCloseable {
/**
* Get the address of the back end debugger
*
* @return the address, usually IP of the host and port for the trace-rmi plugin.
*/
SocketAddress getRemoteAddress();
/**
* Get the methods provided by the back end
*
* @return the method registry
*/
RemoteMethodRegistry getMethods();
/**
* Wait for the first trace created by the back end.
*
* <p>
* Typically, a connection handles only a single target. A shell script handles launching the
* back-end debugger, creating its first target, and connecting back to the front end via
* TraceRmi. If a secondary target does appear, it usually happens only after the initial target
* has run. Thus, this method is useful for waiting on and getting and handle to that initial
* target.
*
* @param timeoutMillis the number of milliseconds to wait for the target
* @return the trace
* @throws TimeoutException if no trace is created after the given timeout. This usually
* indicates there was an error launching the initial target, e.g., the target's
* binary was not found on the target's host.
*/
Trace waitForTrace(long timeoutMillis) throws TimeoutException;
/**
* Get the last snapshot created by the back end for the given trace.
*
* <p>
* Back ends that support timeless or time-travel debugging have not been integrated yet, but in
* those cases, we anticipate this method returning the current snapshot (however the back end
* defines that with respect to its own definition of time), whether or not it is the last
* snapshot it created. If the back end has not created a snapshot yet, 0 is returned.
*
* @param trace
* @return the snapshot number
* @throws NoSuchElementException if the given trace is not a target for this connection
*/
long getLastSnapshot(Trace trace);
/**
* Forcefully remove the given trace from the connection.
*
* <p>
* This removes the back end's access to the given trace and removes this connection from the
* trace's list of consumers (thus, freeing it if this was the only remaining consumer.) For all
* intents and purposes, the given trace is no longer a target for this connection.
*
* <p>
* <b>NOTE:</b> This method should only be used if gracefully killing the target has failed. In
* some cases, it may be better to terminate the entire connection (See {@link #close()}) or to
* terminate the back end debugger. The back end gets no notification that its trace was
* forcefully removed. However, subsequent requests involving that trace will result in errors.
*
* @param trace the trace to remove
*/
void forceCloseTrace(Trace trace);
/**
* Close the TraceRmi connection.
*
* <p>
* {@inheritDoc}
*
* <p>
* Upon closing, all the connection's targets (there's usually only one) will be withdrawn and
* invalidated.
*/
@Override
void close() throws IOException;
/**
* Check if the connection has been closed
*
* @return true if closed, false if still open/valid
*/
boolean isClosed();
/**
* Wait for the connection to become closed.
*
* <p>
* This is usually just for clean-up purposes during automated testing.
*/
void waitClosed();
/**
* Check if the given trace represents one of this connection's targets.
*
* @param trace the trace
* @return true if the trace is a target, false otherwise.
*/
boolean isTarget(Trace trace);
}
@@ -15,7 +15,6 @@
*/
package ghidra.debug.api.tracermi;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@@ -37,19 +36,6 @@ import ghidra.util.task.TaskMonitor;
*/
public interface TraceRmiLaunchOffer {
/**
* A terminal with some back-end element attached to it
*/
interface TerminalSession extends AutoCloseable {
@Override
void close() throws IOException;
/**
* Terminate the session without closing the terminal
*/
void terminate() throws IOException;
}
/**
* The result of launching a program
*
@@ -1,221 +0,0 @@
/* ###
* 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.debug.api.workflow;
import ghidra.app.services.DebuggerWorkflowFrontEndService;
import ghidra.framework.options.AutoOptions;
import ghidra.framework.plugintool.PluginTool;
import ghidra.lifecycle.Internal;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Trace;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.classfinder.ExtensionPoint;
/**
* A bot (or analyzer) that aids the user in the debugging workflow
*
* <p>
* These are a sort of miniature front-end plugin (TODO: consider tool-only bots) with a number of
* conveniences allowing the specification of automatic actions taken under given circumstances,
* e.g., "Open the interpreter for new debugger connections." Such actions may include analysis of
* open traces, e.g., "Disassemble memory at the Program Counter."
*
* <p>
* Bots which react to target state changes should take care to act quickly in most, if not all,
* circumstances. Otherwise, the UI could become sluggish. It is vitally important that the UI not
* become sluggish when the user is stepping a target. Bots should also be wary of prompts. If too
* many bots are prompting the user for input, they may collectively become a source of extreme
* annoyance. In most cases, the bot should use its best judgment and just perform the action, so
* long as it's not potentially destructive. That way, the user can undo the action and/or disable
* the bot. For cases where the bot, in its best judgment, cannot make a decision, it's probably
* best to simply log an informational message and do nothing. There are exceptions, just consider
* them carefully, and be mindful of prompting the user unexpectedly or incessantly.
*/
public interface DebuggerBot extends ExtensionPoint {
/**
* Log a missing-info-annotation error
*
* @param cls the bot's class missing the annotation
* @param methodName the name of the method requesting the info
*/
@Internal
static void noAnnot(Class<?> cls, String methodName) {
Msg.error(DebuggerBot.class, "Debugger bot " + cls + " must apply @" +
DebuggerBotInfo.class.getSimpleName() + " or override getDescription()");
}
/**
* Utility for obtaining and bot's info annotation
*
* <p>
* If the annotation is not present, an error is logged for the developer's sake.
*
* @param cls the bot's class
* @param methodName the name of the method requesting the info, for error-reporting purposes
* @return the annotation, or {@code null}
*/
@Internal
static DebuggerBotInfo getInfo(Class<?> cls, String methodName) {
DebuggerBotInfo info = cls.getAnnotation(DebuggerBotInfo.class);
if (info == null) {
noAnnot(cls, methodName);
}
return info;
}
/**
* Get a description of the bot
*
* @see DebuggerBotInfo#description()
* @return the description
*/
default String getDescription() {
DebuggerBotInfo info = getInfo(getClass(), "getDescription");
if (info == null) {
return "<NO DESCRIPTION>";
}
return info.description();
}
/**
* Get a detailed description of the bot
*
* @see DebuggerBotInfo#details()
* @return the details
*/
default String getDetails() {
DebuggerBotInfo info = getInfo(getClass(), "getDetails");
if (info == null) {
return "";
}
return info.details();
}
/**
* Get the help location for information about the bot
*
* @see DebuggerBotInfo#help()
* @return the help location
*/
default HelpLocation getHelpLocation() {
DebuggerBotInfo info = getInfo(getClass(), "getHelpLocation");
if (info == null) {
return null;
}
return AutoOptions.getHelpLocation("DebuggerBots", info.help());
}
/**
* Check whether this bot is enabled by default
*
* <p>
* Assuming the user has never configured this bot before, determine whether it should be
* enabled.
*
* @return true if enabled by default, false otherwise
*/
default boolean isEnabledByDefault() {
DebuggerBotInfo info = getInfo(getClass(), "isEnabledByDefault");
if (info == null) {
return false;
}
return info.enabledByDefault();
}
/**
* Check if this bot is enabled
*
* @return true if enabled, false otherwise
*/
boolean isEnabled();
/**
* Enable or disable the bot
*
* <p>
* If {@link #isEnabled()} is already equal to the given -enabled- value, this method has no
* effect.
*
* @param service the front-end service, required if -enabled- is set
* @param enabled true to enable, false to disable
*/
default void setEnabled(DebuggerWorkflowFrontEndService service, boolean enabled) {
if (isEnabled() == enabled) {
return;
}
if (enabled) {
enable(service);
}
else {
disable();
}
}
/**
* Enable and initialize the bot
*
* @param service the front-end service
*/
void enable(DebuggerWorkflowFrontEndService service);
/**
* Disable and dispose the bot
*
* <p>
* Note the bot must be prepared to be enabled again. In other words, it will not be
* re-instantiated. It should return to the same state after construction but before being
* enabled the first time.
*/
void disable();
/**
* A program has been opened in a tool
*
* @param tool the tool which opened the program
* @param program the program that was opened
*/
default void programOpened(PluginTool tool, Program program) {
}
/**
* A program has been closed in a tool
*
* @param tool the tool which closed the program
* @param program the program that was closed
*/
default void programClosed(PluginTool tool, Program program) {
}
/**
* A trace has been opened in a tool
*
* @param tool the tool which opened the trace
* @param trace the trace that was opened
*/
default void traceOpened(PluginTool tool, Trace trace) {
}
/**
* A trace has been closed in a tool
*
* @param tool the tool which closed the trace
* @param trace the trace that was closed
*/
default void traceClosed(PluginTool tool, Trace trace) {
}
}
@@ -1,76 +0,0 @@
/* ###
* 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.debug.api.workflow;
import java.lang.annotation.*;
import ghidra.framework.options.annotation.HelpInfo;
/**
* Required information annotation on {@link DebuggerBot}s
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DebuggerBotInfo {
/**
* A quick one-line description of the actor
*
* This is used as the option name to enable and disable the actor, to please, keep it short.
* Use {@link #details()} or {@link #help()} to provide more details.
*
* @return the description
*/
String description();
/**
* A longer description of this actor
*
* A one-to-three-sentence detailed description of the actor. Again, it should be relatively
* short, as it used as the tool-tip popup in the plugin's options dialog. On some systems, such
* tips only display for a short time.
*
* @return the detailed description
*/
String details();
/**
* The location for help about this actor
*
* Help is the best place to put lengthy descriptions of the actor and/or describe the caveats
* of using it. Since, in most cases, the actor is simply performing automatic actions, it is
* useful to show the reader how to perform those same actions manually. This way, if/when the
* actor takes an unreasonable action, the user can manually correct it.
*
* @return the link to detailed help about the actor
*/
HelpInfo help() default @HelpInfo(topic = {});
/**
* Check whether the actor should be enabled by default
*
* For the stock plugin, a collection of actors should be enabled by default that make the
* debugger most accessible, erring toward ease of use, rather than toward correctness. Advanced
* users can always disable unwanted actors, tweak the options (TODO: Allow actors to present
* additional options in the tool config), and/or write their own actors and scripts.
*
* For extensions, consider the user's expectations upon installing your extension. For example,
* if the extension consists of just an actor and some supporting classes, it should probably be
* enabled by default.
*
* @return true to enable by default, false to leave disabled by default
*/
boolean enabledByDefault() default false;
}
+28 -1
View File
@@ -280,8 +280,35 @@ For the user to open a second transaction may be considered an error.
Take care as you're coding (and likely re-using command logic) that you don't accidentally take or otherwise conflict with the CLI's transaction manager when processing an event.
# Regarding launcher shell scripts:
Need to document all the @metadata stuff
In particular, "Image" is a special parameter that will get the program executable by default.
# Regarding the schema and method signatures
An interface like Togglable requires that a TOGGLE action takes the given schema as a parameter
(e.g., a BreakpointLocation)
# Regarding registers
The register container has to exist, even if its left empty in favor of the register space.
1. The space is named after the container
2. The UI uses the container as an anchor in its searches
To allow register writes, each writable register must exist as an object it the register container.
1. I might like to relax this....
2. The UI it to validate the register is editable, even though the RemoteMethod may accept
frame/thread,name,val.
# Regarding reading and writing memory
The process parameter, if accepted, is technically redundant.
Because all address spaces among all targets must be unique, the address space encodes the process (or other target object).
If taken, the back end must validate that the address space belongs to the given process.
Otherwise, the back end must figure out the applicable target based on the space name.
@@ -0,0 +1,387 @@
/* ###
* 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.app.plugin.core.debug.gui.tracermi;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.beans.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.jdom.Element;
import docking.DialogComponentProvider;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.utils.MiscellaneousUtils;
import ghidra.dbg.target.schema.SchemaContext;
import ghidra.debug.api.tracermi.RemoteParameter;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.AutoConfigState.ConfigStateField;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.Msg;
import ghidra.util.layout.PairLayout;
public class RemoteMethodInvocationDialog extends DialogComponentProvider
implements PropertyChangeListener {
private static final String KEY_MEMORIZED_ARGUMENTS = "memorizedArguments";
static class ChoicesPropertyEditor implements PropertyEditor {
private final List<?> choices;
private final String[] tags;
private final List<PropertyChangeListener> listeners = new ArrayList<>();
private Object value;
public ChoicesPropertyEditor(Set<?> choices) {
this.choices = List.copyOf(choices);
this.tags = choices.stream().map(Objects::toString).toArray(String[]::new);
}
@Override
public void setValue(Object value) {
if (Objects.equals(value, this.value)) {
return;
}
if (!choices.contains(value)) {
throw new IllegalArgumentException("Unsupported value: " + value);
}
Object oldValue;
List<PropertyChangeListener> listeners;
synchronized (this.listeners) {
oldValue = this.value;
this.value = value;
if (this.listeners.isEmpty()) {
return;
}
listeners = List.copyOf(this.listeners);
}
PropertyChangeEvent evt = new PropertyChangeEvent(this, null, oldValue, value);
for (PropertyChangeListener l : listeners) {
l.propertyChange(evt);
}
}
@Override
public Object getValue() {
return value;
}
@Override
public boolean isPaintable() {
return false;
}
@Override
public void paintValue(Graphics gfx, Rectangle box) {
// Not paintable
}
@Override
public String getJavaInitializationString() {
if (value == null) {
return "null";
}
if (value instanceof String str) {
return "\"" + StringEscapeUtils.escapeJava(str) + "\"";
}
return Objects.toString(value);
}
@Override
public String getAsText() {
return Objects.toString(value);
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
int index = ArrayUtils.indexOf(tags, text);
if (index < 0) {
throw new IllegalArgumentException("Unsupported value: " + text);
}
setValue(choices.get(index));
}
@Override
public String[] getTags() {
return tags.clone();
}
@Override
public Component getCustomEditor() {
return null;
}
@Override
public boolean supportsCustomEditor() {
return false;
}
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
}
@Override
public void removePropertyChangeListener(PropertyChangeListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
}
record NameTypePair(String name, Class<?> type) {
public static NameTypePair fromParameter(SchemaContext ctx, RemoteParameter parameter) {
return new NameTypePair(parameter.name(), ctx.getSchema(parameter.type()).getType());
}
public static NameTypePair fromString(String name) throws ClassNotFoundException {
String[] parts = name.split(",", 2);
if (parts.length != 2) {
// This appears to be a bad assumption - empty fields results in solitary labels
return new NameTypePair(parts[0], String.class);
//throw new IllegalArgumentException("Could not parse name,type");
}
return new NameTypePair(parts[0], Class.forName(parts[1]));
}
}
private final BidiMap<RemoteParameter, PropertyEditor> paramEditors =
new DualLinkedHashBidiMap<>();
private JPanel panel;
private JLabel descriptionLabel;
private JPanel pairPanel;
private PairLayout layout;
protected JButton invokeButton;
protected JButton resetButton;
private final PluginTool tool;
private SchemaContext ctx;
private Map<String, RemoteParameter> parameters;
private Map<String, Object> defaults;
// TODO: Not sure this is the best keying, but I think it works.
private Map<NameTypePair, Object> memorized = new HashMap<>();
private Map<String, Object> arguments;
public RemoteMethodInvocationDialog(PluginTool tool, String title, String buttonText,
Icon buttonIcon) {
super(title, true, true, true, false);
this.tool = tool;
populateComponents(buttonText, buttonIcon);
setRememberSize(false);
}
protected Object computeMemorizedValue(RemoteParameter parameter) {
return memorized.computeIfAbsent(NameTypePair.fromParameter(ctx, parameter),
ntp -> parameter.getDefaultValue());
}
public Map<String, Object> promptArguments(SchemaContext ctx,
Map<String, RemoteParameter> parameterMap, Map<String, Object> defaults) {
setParameters(ctx, parameterMap);
setDefaults(defaults);
tool.showDialog(this);
return getArguments();
}
public void setParameters(SchemaContext ctx, Map<String, RemoteParameter> parameterMap) {
this.ctx = ctx;
this.parameters = parameterMap;
populateOptions();
}
public void setDefaults(Map<String, Object> defaults) {
this.defaults = defaults;
}
private void populateComponents(String buttonText, Icon buttonIcon) {
panel = new JPanel(new BorderLayout());
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
layout = new PairLayout(5, 5);
pairPanel = new JPanel(layout);
JPanel centering = new JPanel(new FlowLayout(FlowLayout.CENTER));
JScrollPane scrolling = new JScrollPane(centering, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
//scrolling.setPreferredSize(new Dimension(100, 130));
panel.add(scrolling, BorderLayout.CENTER);
centering.add(pairPanel);
descriptionLabel = new JLabel();
descriptionLabel.setMaximumSize(new Dimension(300, 100));
panel.add(descriptionLabel, BorderLayout.NORTH);
addWorkPanel(panel);
invokeButton = new JButton(buttonText, buttonIcon);
addButton(invokeButton);
resetButton = new JButton("Reset", DebuggerResources.ICON_REFRESH);
addButton(resetButton);
addCancelButton();
invokeButton.addActionListener(this::invoke);
resetButton.addActionListener(this::reset);
}
@Override
protected void cancelCallback() {
this.arguments = null;
close();
}
protected void invoke(ActionEvent evt) {
this.arguments = collectArguments();
close();
}
private void reset(ActionEvent evt) {
this.arguments = new HashMap<>();
for (RemoteParameter param : parameters.values()) {
if (defaults.containsKey(param.name())) {
arguments.put(param.name(), defaults.get(param.name()));
}
else {
arguments.put(param.name(), param.getDefaultValue());
}
}
populateValues();
}
protected PropertyEditor createEditor(RemoteParameter param) {
Class<?> type = ctx.getSchema(param.type()).getType();
PropertyEditor editor = PropertyEditorManager.findEditor(type);
if (editor != null) {
return editor;
}
Msg.warn(this, "No editor for " + type + "? Trying String instead");
return PropertyEditorManager.findEditor(String.class);
}
void populateOptions() {
pairPanel.removeAll();
paramEditors.clear();
for (RemoteParameter param : parameters.values()) {
JLabel label = new JLabel(param.display());
label.setToolTipText(param.description());
pairPanel.add(label);
PropertyEditor editor = createEditor(param);
Object val = computeMemorizedValue(param);
editor.setValue(val);
editor.addPropertyChangeListener(this);
pairPanel.add(MiscellaneousUtils.getEditorComponent(editor));
paramEditors.put(param, editor);
}
}
void populateValues() {
for (Map.Entry<String, Object> ent : arguments.entrySet()) {
RemoteParameter param = parameters.get(ent.getKey());
if (param == null) {
Msg.warn(this, "No parameter for argument: " + ent);
continue;
}
PropertyEditor editor = paramEditors.get(param);
editor.setValue(ent.getValue());
}
}
protected Map<String, Object> collectArguments() {
Map<String, Object> map = new LinkedHashMap<>();
for (RemoteParameter param : paramEditors.keySet()) {
Object val = memorized.get(NameTypePair.fromParameter(ctx, param));
if (val != null) {
map.put(param.name(), val);
}
}
return map;
}
public Map<String, Object> getArguments() {
return arguments;
}
public <T> void setMemorizedArgument(String name, Class<T> type, T value) {
if (value == null) {
return;
}
memorized.put(new NameTypePair(name, type), value);
}
public <T> T getMemorizedArgument(String name, Class<T> type) {
return type.cast(memorized.get(new NameTypePair(name, type)));
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
PropertyEditor editor = (PropertyEditor) evt.getSource();
RemoteParameter param = paramEditors.getKey(editor);
memorized.put(NameTypePair.fromParameter(ctx, param), editor.getValue());
}
public void writeConfigState(SaveState saveState) {
SaveState subState = new SaveState();
for (Map.Entry<NameTypePair, Object> ent : memorized.entrySet()) {
NameTypePair ntp = ent.getKey();
ConfigStateField.putState(subState, ntp.type().asSubclass(Object.class), ntp.name(),
ent.getValue());
}
saveState.putXmlElement(KEY_MEMORIZED_ARGUMENTS, subState.saveToXml());
}
public void readConfigState(SaveState saveState) {
Element element = saveState.getXmlElement(KEY_MEMORIZED_ARGUMENTS);
if (element == null) {
return;
}
SaveState subState = new SaveState(element);
for (String name : subState.getNames()) {
try {
NameTypePair ntp = NameTypePair.fromString(name);
memorized.put(ntp, ConfigStateField.getState(subState, ntp.type(), ntp.name()));
}
catch (Exception e) {
Msg.error(this, "Error restoring memorized parameter " + name, e);
}
}
}
public void setDescription(String htmlDescription) {
if (htmlDescription == null) {
descriptionLabel.setBorder(BorderFactory.createEmptyBorder());
descriptionLabel.setText("");
}
else {
descriptionLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
descriptionLabel.setText(htmlDescription);
}
}
}
@@ -30,10 +30,14 @@ import org.jdom.Element;
import org.jdom.JDOMException;
import db.Transaction;
import docking.widgets.OptionDialog;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
import ghidra.app.plugin.core.debug.service.rmi.trace.DefaultTraceRmiAcceptor;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
import ghidra.app.plugin.core.terminal.TerminalListener;
import ghidra.app.services.*;
import ghidra.app.services.DebuggerTraceManagerService.ActivationCause;
import ghidra.async.AsyncUtils;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.util.ShellUtils;
@@ -50,8 +54,7 @@ import ghidra.pty.*;
import ghidra.trace.model.Trace;
import ghidra.trace.model.TraceLocation;
import ghidra.trace.model.modules.TraceModule;
import ghidra.util.MessageType;
import ghidra.util.Msg;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
@@ -77,6 +80,16 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
pty.close();
waiter.interrupt();
}
@Override
public boolean isTerminated() {
return terminal.isTerminated();
}
@Override
public String description() {
return session.description();
}
}
protected record NullPtyTerminalSession(Terminal terminal, Pty pty, String name)
@@ -92,6 +105,16 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
terminal.terminated();
pty.close();
}
@Override
public boolean isTerminated() {
return terminal.isTerminated();
}
@Override
public String description() {
return name;
}
}
static class TerminateSessionTask extends Task {
@@ -113,13 +136,15 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
}
}
protected final TraceRmiLauncherServicePlugin plugin;
protected final Program program;
protected final PluginTool tool;
protected final TerminalService terminalService;
public AbstractTraceRmiLaunchOffer(Program program, PluginTool tool) {
public AbstractTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program) {
this.plugin = Objects.requireNonNull(plugin);
this.program = Objects.requireNonNull(program);
this.tool = Objects.requireNonNull(tool);
this.tool = plugin.getTool();
this.terminalService = Objects.requireNonNull(tool.getService(TerminalService.class));
}
@@ -151,9 +176,8 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
return null; // I guess we won't wait for a mapping, then
}
protected CompletableFuture<Void> listenForMapping(
DebuggerStaticMappingService mappingService, TraceRmiConnection connection,
Trace trace) {
protected CompletableFuture<Void> listenForMapping(DebuggerStaticMappingService mappingService,
TraceRmiConnection connection, Trace trace) {
Address probeAddress = getMappingProbeAddress();
if (probeAddress == null) {
return AsyncUtils.nil(); // No need to wait on mapping of nothing
@@ -469,9 +493,20 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
Map<String, TerminalSession> sessions, Map<String, ?> args, SocketAddress address)
throws Exception;
static class NoStaticMappingException extends Exception {
public NoStaticMappingException(String message) {
super(message);
}
@Override
public String toString() {
return getMessage();
}
}
@Override
public LaunchResult launchProgram(TaskMonitor monitor, LaunchConfigurator configurator) {
TraceRmiService service = tool.getService(TraceRmiService.class);
InternalTraceRmiService service = tool.getService(InternalTraceRmiService.class);
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
DebuggerTraceManagerService traceManager =
@@ -479,9 +514,9 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
final PromptMode mode = configurator.getPromptMode();
boolean prompt = mode == PromptMode.ALWAYS;
TraceRmiAcceptor acceptor = null;
DefaultTraceRmiAcceptor acceptor = null;
Map<String, TerminalSession> sessions = new LinkedHashMap<>();
TraceRmiConnection connection = null;
TraceRmiHandler connection = null;
Trace trace = null;
Throwable lastExc = null;
@@ -509,10 +544,12 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
monitor.setMessage("Waiting for connection");
acceptor.setTimeout(getTimeoutMillis());
connection = acceptor.accept();
connection.registerTerminals(sessions.values());
monitor.setMessage("Waiting for trace");
trace = connection.waitForTrace(getTimeoutMillis());
traceManager.openTrace(trace);
traceManager.activateTrace(trace);
traceManager.activate(traceManager.resolveTrace(trace),
ActivationCause.START_RECORDING);
monitor.setMessage("Waiting for module mapping");
try {
listenForMapping(mappingService, connection, trace).get(getTimeoutMillis(),
@@ -529,25 +566,132 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
throw new CancellationException(e.getMessage());
}
if (mapped.isEmpty()) {
monitor.setMessage(
"Could not formulate a mapping with the target program. " +
"Continuing without one.");
Msg.showWarn(this, null, "Launch " + program,
"The resulting target process has no mapping to the static image " +
program + ". Intervention is required before static and dynamic " +
"addresses can be translated. Check the target's module list.");
throw new NoStaticMappingException(
"The resulting target process has no mapping to the static image.");
}
}
}
catch (Exception e) {
lastExc = e;
prompt = mode != PromptMode.NEVER;
LaunchResult result =
new LaunchResult(program, sessions, connection, trace, lastExc);
if (prompt) {
switch (promptError(result)) {
case KEEP:
return result;
case RETRY:
try {
result.close();
}
catch (Exception e1) {
Msg.error(this, "Could not close", e1);
}
continue;
case TERMINATE:
try {
result.close();
}
catch (Exception e1) {
Msg.error(this, "Could not close", e1);
}
return new LaunchResult(program, Map.of(), null, null, lastExc);
}
continue;
}
return new LaunchResult(program, sessions, connection, trace, lastExc);
return result;
}
return new LaunchResult(program, sessions, connection, trace, null);
}
}
enum ErrPromptResponse {
KEEP, RETRY, TERMINATE;
}
protected ErrPromptResponse promptError(LaunchResult result) {
String message = """
<html><body width="400px">
<h3>Failed to launch %s due to an exception:</h3>
<tt>%s</tt>
<h3>Troubleshooting</h3>
<p>
<b>Check the Terminal!</b>
If no terminal is visible, check the menus: <b>Window &rarr; Terminals &rarr;
...</b>.
A path or other configuration parameter may be incorrect.
The back-end debugger may have paused for user input.
There may be a missing dependency.
There may be an incorrect version, etc.</p>
<h3>These resources remain after the failed launch:</h3>
<ul>
%s
</ul>
<h3>Do you want to keep these resources?</h3>
<ul>
<li>Choose <b>Yes</b> to stop here and diagnose or complete the launch manually.
</li>
<li>Choose <b>No</b> to clean up and retry at the launch dialog.</li>
<li>Choose <b>Cancel</b> to clean up without retrying.</li>
""".formatted(
htmlProgramName(result), htmlExceptionMessage(result), htmlResources(result));
return LaunchFailureDialog.show(message);
}
static class LaunchFailureDialog extends OptionDialog {
public LaunchFailureDialog(String message) {
super("Launch Failed", message, "&Yes", "&No", OptionDialog.ERROR_MESSAGE, null,
true, "No");
}
static ErrPromptResponse show(String message) {
return switch (new LaunchFailureDialog(message).show()) {
case OptionDialog.YES_OPTION -> ErrPromptResponse.KEEP;
case OptionDialog.NO_OPTION -> ErrPromptResponse.RETRY;
case OptionDialog.CANCEL_OPTION -> ErrPromptResponse.TERMINATE;
default -> throw new AssertionError();
};
}
}
protected String htmlProgramName(LaunchResult result) {
if (result.program() == null) {
return "";
}
return "<tt>" + HTMLUtilities.escapeHTML(result.program().getName()) + "</tt>";
}
protected String htmlExceptionMessage(LaunchResult result) {
if (result.exception() == null) {
return "(No exception)";
}
return HTMLUtilities.escapeHTML(result.exception().toString());
}
protected String htmlResources(LaunchResult result) {
StringBuilder sb = new StringBuilder();
for (Entry<String, TerminalSession> ent : result.sessions().entrySet()) {
TerminalSession session = ent.getValue();
sb.append("<li>Terminal: " + HTMLUtilities.escapeHTML(ent.getKey()) + " &rarr; <tt>" +
HTMLUtilities.escapeHTML(session.description()) + "</tt>");
if (session.isTerminated()) {
sb.append(" (Terminated)");
}
sb.append("</li>\n");
}
if (result.connection() != null) {
sb.append("<li>Connection: <tt>" +
HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString()) +
"</tt></li>\n");
}
if (result.trace() != null) {
sb.append(
"<li>Trace: " + HTMLUtilities.escapeHTML(result.trace().getName()) + "</li>\n");
}
return sb.toString();
}
}
@@ -31,7 +31,7 @@ import ghidra.app.services.*;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.LaunchConfigurator;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.PromptMode;
import ghidra.debug.api.tracermi.TraceRmiLaunchOpinion;
import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion;
import ghidra.framework.options.OptionsChangeListener;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.*;
@@ -67,6 +67,13 @@ public class TraceRmiLauncherServicePlugin extends Plugin
implements TraceRmiLauncherService, OptionsChangeListener {
protected static final String OPTION_NAME_SCRIPT_PATHS = "Script Paths";
private final static LaunchConfigurator RELAUNCH = new LaunchConfigurator() {
@Override
public PromptMode getPromptMode() {
return PromptMode.ON_ERROR;
}
};
private final static LaunchConfigurator PROMPT = new LaunchConfigurator() {
@Override
public PromptMode getPromptMode() {
@@ -90,7 +97,7 @@ public class TraceRmiLauncherServicePlugin extends Plugin
@Override
public void run(TaskMonitor monitor) throws CancelledException {
offer.launchProgram(monitor);
offer.launchProgram(monitor, RELAUNCH);
}
}
@@ -165,11 +172,6 @@ public class TraceRmiLauncherServicePlugin extends Plugin
}
}
@Override
public Collection<TraceRmiLaunchOpinion> getOpinions() {
return ClassSearcher.getInstances(TraceRmiLaunchOpinion.class);
}
@Override
public Collection<TraceRmiLaunchOffer> getOffers(Program program) {
if (program == null) {
@@ -177,7 +179,7 @@ public class TraceRmiLauncherServicePlugin extends Plugin
}
return ClassSearcher.getInstances(TraceRmiLaunchOpinion.class)
.stream()
.flatMap(op -> op.getOffers(program, getTool()).stream())
.flatMap(op -> op.getOffers(this, program).stream())
.toList();
}
@@ -27,8 +27,8 @@ import generic.theme.GIcon;
import generic.theme.Gui;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.util.ShellUtils;
import ghidra.debug.api.tracermi.TerminalSession;
import ghidra.framework.Application;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
@@ -471,8 +471,8 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOf
* the target image is mapped in the resulting target trace.
* @throws FileNotFoundException
*/
public static UnixShellScriptTraceRmiLaunchOffer create(Program program, PluginTool tool,
File script) throws FileNotFoundException {
public static UnixShellScriptTraceRmiLaunchOffer create(TraceRmiLauncherServicePlugin plugin,
Program program, File script) throws FileNotFoundException {
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(new FileInputStream(script)))) {
AttributesParser attrs = new AttributesParser();
@@ -491,7 +491,7 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOf
}
}
attrs.validate(script.getName());
return new UnixShellScriptTraceRmiLaunchOffer(program, tool, script,
return new UnixShellScriptTraceRmiLaunchOffer(plugin, program, script,
"UNIX_SHELL:" + script.getName(), attrs.title, attrs.getDescription(),
attrs.menuPath, attrs.menuGroup, attrs.menuOrder, new GIcon(attrs.iconId),
attrs.helpLocation, attrs.parameters, attrs.extraTtys);
@@ -517,11 +517,11 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOf
protected final Map<String, ParameterDescription<?>> parameters;
protected final List<String> extraTtys;
public UnixShellScriptTraceRmiLaunchOffer(Program program, PluginTool tool, File script,
String configName, String title, String description, List<String> menuPath,
public UnixShellScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program,
File script, String configName, String title, String description, List<String> menuPath,
String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation,
Map<String, ParameterDescription<?>> parameters, Collection<String> extraTtys) {
super(program, tool);
super(plugin, program);
this.script = script;
this.configName = configName;
this.title = title;
@@ -22,7 +22,7 @@ import java.util.stream.Stream;
import generic.jar.ResourceFile;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.debug.api.tracermi.TraceRmiLaunchOpinion;
import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion;
import ghidra.framework.Application;
import ghidra.framework.options.OptionType;
import ghidra.framework.options.Options;
@@ -63,12 +63,13 @@ public class UnixShellScriptTraceRmiLaunchOpinion implements TraceRmiLaunchOpini
}
@Override
public Collection<TraceRmiLaunchOffer> getOffers(Program program, PluginTool tool) {
return getScriptPaths(tool)
public Collection<TraceRmiLaunchOffer> getOffers(TraceRmiLauncherServicePlugin plugin,
Program program) {
return getScriptPaths(plugin.getTool())
.flatMap(rf -> Stream.of(rf.listFiles(crf -> crf.getName().endsWith(".sh"))))
.flatMap(sf -> {
try {
return Stream.of(UnixShellScriptTraceRmiLaunchOffer.create(program, tool,
return Stream.of(UnixShellScriptTraceRmiLaunchOffer.create(plugin, program,
sf.getFile(false)));
}
catch (Exception e) {
@@ -36,11 +36,14 @@ import com.google.protobuf.ByteString;
import db.Transaction;
import ghidra.app.plugin.core.debug.disassemble.DebuggerDisassemblerPlugin;
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
import ghidra.app.services.DebuggerControlService;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.app.services.DebuggerTraceManagerService.ActivationCause;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.dbg.target.schema.XmlSchemaContext;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathUtils;
import ghidra.debug.api.control.ControlMode;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.debug.api.tracermi.*;
@@ -48,7 +51,6 @@ import ghidra.framework.model.*;
import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.AutoService.Wiring;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.lifecycle.Internal;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.*;
import ghidra.program.util.DefaultLanguageService;
@@ -158,6 +160,16 @@ public class TraceRmiHandler implements TraceRmiConnection {
return removed;
}
public synchronized OpenTrace removeByTrace(Trace trace) {
OpenTrace removed = byTrace.remove(trace);
if (removed == null) {
return null;
}
byId.remove(removed.doId);
plugin.withdrawTarget(removed.target);
return removed;
}
public synchronized OpenTrace getById(DoId doId) {
return byId.get(doId);
}
@@ -185,6 +197,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
private final OutputStream out;
private final CompletableFuture<Void> negotiate = new CompletableFuture<>();
private final CompletableFuture<Void> closed = new CompletableFuture<>();
private final Set<TerminalSession> terminals = new LinkedHashSet<>();
private final OpenTraceMap openTraces = new OpenTraceMap();
private final Map<Tid, OpenTx> openTxes = new HashMap<>();
@@ -195,6 +208,8 @@ public class TraceRmiHandler implements TraceRmiConnection {
@AutoServiceConsumed
private DebuggerTraceManagerService traceManager;
@AutoServiceConsumed
private DebuggerControlService controlService;
@SuppressWarnings("unused")
private final Wiring autoServiceWiring;
@@ -230,9 +245,30 @@ public class TraceRmiHandler implements TraceRmiConnection {
}
}
protected void terminateTerminals() {
List<TerminalSession> terminals;
synchronized (this.terminals) {
terminals = List.copyOf(this.terminals);
this.terminals.clear();
}
for (TerminalSession term : terminals) {
CompletableFuture.runAsync(() -> {
try {
term.terminate();
}
catch (Exception e) {
Msg.error(this, "Could not terminate " + term + ": " + e);
}
});
}
}
public void dispose() throws IOException {
plugin.removeHandler(this);
flushXReqQueue(new TraceRmiError("Socket closed"));
terminateTerminals();
socket.close();
while (!openTxes.isEmpty()) {
Tid nextKey = openTxes.keySet().iterator().next();
@@ -467,6 +503,10 @@ public class TraceRmiHandler implements TraceRmiConnection {
case REQUEST_START_TX -> "startTx(%d,%s)".formatted(
req.getRequestStartTx().getTxid().getId(),
req.getRequestStartTx().getDescription());
case REQUEST_SET_VALUE -> "setValue(%d,%s,%s)".formatted(
req.getRequestSetValue().getValue().getParent().getId(),
req.getRequestSetValue().getValue().getParent().getPath().getPath(),
req.getRequestSetValue().getValue().getKey());
default -> null;
};
}
@@ -751,25 +791,26 @@ public class TraceRmiHandler implements TraceRmiConnection {
OpenTrace open = requireOpenTrace(req.getOid());
TraceObject object = open.getObject(req.getObject(), true);
DebuggerCoordinates coords = traceManager.getCurrent();
if (coords.getTrace() == object.getTrace()) {
coords = coords.object(object);
if (coords.getTrace() != open.trace) {
coords = DebuggerCoordinates.NOWHERE;
}
else {
coords = DebuggerCoordinates.NOWHERE.object(object);
}
if (open.lastSnapshot != null) {
ControlMode mode = controlService.getCurrentMode(open.trace);
if (open.lastSnapshot != null && mode.followsPresent()) {
coords = coords.snap(open.lastSnapshot.getKey());
}
if (!traceManager.getOpenTraces().contains(open.trace)) {
traceManager.openTrace(open.trace);
traceManager.activate(coords);
}
else {
Trace currentTrace = traceManager.getCurrentTrace();
if (currentTrace == null || openTraces.getByTrace(currentTrace) != null) {
traceManager.activate(coords);
DebuggerCoordinates finalCoords = coords.object(object);
Swing.runLater(() -> {
if (!traceManager.getOpenTraces().contains(open.trace)) {
traceManager.openTrace(open.trace);
traceManager.activate(finalCoords, ActivationCause.SYNC_MODEL);
}
}
else {
Trace currentTrace = traceManager.getCurrentTrace();
if (currentTrace == null || openTraces.getByTrace(currentTrace) != null) {
traceManager.activate(finalCoords, ActivationCause.SYNC_MODEL);
}
}
});
return ReplyActivate.getDefaultInstance();
}
@@ -965,7 +1006,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
}
for (Method m : req.getMethodsList()) {
RemoteMethod rm = new RecordRemoteMethod(this, m.getName(),
new ActionName(m.getAction()),
ActionName.name(m.getAction()),
m.getDescription(), m.getParametersList()
.stream()
.collect(Collectors.toMap(MethodParameter::getName, this::makeParameter)),
@@ -996,7 +1037,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
for (RegVal rv : req.getValuesList()) {
Register register = open.getRegister(rv.getName(), false);
if (register == null) {
Msg.warn(this, "Ignoring unrecognized register: " + rv.getName());
Msg.trace(this, "Ignoring unrecognized register: " + rv.getName());
rep.addSkippedNames(rv.getName());
continue;
}
@@ -1182,9 +1223,12 @@ public class TraceRmiHandler implements TraceRmiConnection {
}
@Override
@Internal
public long getLastSnapshot(Trace trace) {
TraceSnapshot lastSnapshot = openTraces.getByTrace(trace).lastSnapshot;
OpenTrace byTrace = openTraces.getByTrace(trace);
if (byTrace == null) {
throw new NoSuchElementException();
}
TraceSnapshot lastSnapshot = byTrace.lastSnapshot;
if (lastSnapshot == null) {
return 0;
}
@@ -1200,4 +1244,21 @@ public class TraceRmiHandler implements TraceRmiConnection {
throw new TraceRmiError(e);
}
}
@Override
public void forceCloseTrace(Trace trace) {
OpenTrace open = openTraces.removeByTrace(trace);
open.trace.release(this);
}
@Override
public boolean isTarget(Trace trace) {
return openTraces.getByTrace(trace) != null;
}
public void registerTerminals(Collection<TerminalSession> terminals) {
synchronized (this.terminals) {
this.terminals.addAll(terminals);
}
}
}
@@ -23,8 +23,7 @@ import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
import ghidra.app.services.DebuggerTargetService;
import ghidra.app.services.TraceRmiService;
import ghidra.app.services.*;
import ghidra.debug.api.tracermi.TraceRmiConnection;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.AutoService.Wiring;
@@ -52,8 +51,9 @@ import ghidra.util.task.TaskMonitor;
},
servicesProvided = {
TraceRmiService.class,
InternalTraceRmiService.class,
})
public class TraceRmiPlugin extends Plugin implements TraceRmiService {
public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
private static final int DEFAULT_PORT = 15432;
@AutoServiceConsumed
@@ -0,0 +1,35 @@
/* ###
* 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.app.services;
import java.io.IOException;
import java.net.SocketAddress;
import ghidra.app.plugin.core.debug.service.rmi.trace.DefaultTraceRmiAcceptor;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion;
/**
* The same as the {@link TraceRmiService}, but grants access to the internal types (without
* casting) to implementors of {@link TraceRmiLaunchOpinion}.
*/
public interface InternalTraceRmiService extends TraceRmiService {
@Override
DefaultTraceRmiAcceptor acceptOne(SocketAddress address) throws IOException;
@Override
TraceRmiHandler connect(SocketAddress address) throws IOException;
}
@@ -13,10 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.debug.api.tracermi;
package ghidra.debug.spi.tracermi;
import java.util.Collection;
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLauncherServicePlugin;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
import ghidra.app.services.InternalTraceRmiService;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.framework.options.Options;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
@@ -68,11 +72,16 @@ public interface TraceRmiLaunchOpinion extends ExtensionPoint {
* I.e., the entries there are already validated; they've worked at least once before.</li>
* </ol>
*
* @param plugin the Trace RMI launcher service plugin. <b>NOTE:</b> to get access to the Trace
* RMI (connection) service, use the {@link InternalTraceRmiService}, so that the
* offers can register the connection's resources. See
* {@link TraceRmiHandler#registerResources(Collection)}. Resource registration is
* required for the Disconnect button to completely terminate the back end.
* @param program the current program. While this is not <em>always</em> used by the launcher,
* it is implied that the user expects the debugger to do something with the current
* program, even if it's just informing the back-end debugger of the target image.
* @param tool the current tool for context and services
* @return the offers. The order is not important since items are displayed alphabetically.
* @return the offers. The order is ignored, since items are displayed alphabetically.
*/
public Collection<TraceRmiLaunchOffer> getOffers(Program program, PluginTool tool);
public Collection<TraceRmiLaunchOffer> getOffers(TraceRmiLauncherServicePlugin plugin,
Program program);
}
@@ -17,23 +17,14 @@ import socket
import traceback
def send_all(s, data):
sent = 0
while sent < len(data):
l = s.send(data[sent:])
if l == 0:
raise Exception("Socket closed")
sent += l
def send_length(s, value):
send_all(s, value.to_bytes(4, 'big'))
s.sendall(value.to_bytes(4, 'big'))
def send_delimited(s, msg):
data = msg.SerializeToString()
send_length(s, len(data))
send_all(s, data)
s.sendall(data)
def recv_all(s, size):
@@ -44,7 +35,7 @@ def recv_all(s, size):
return buf
buf += part
return buf
#return s.recv(size, socket.MSG_WAITALL)
# return s.recv(size, socket.MSG_WAITALL)
def recv_length(s):
@@ -0,0 +1,188 @@
/* ###
* 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.app.plugin.core.debug.service.rmi.trace;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import ghidra.app.services.DebuggerTargetService;
import ghidra.async.AsyncPairingQueue;
import ghidra.async.AsyncUtils;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.tracermi.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.trace.model.Trace;
public class TestTraceRmiConnection implements TraceRmiConnection {
protected final TestRemoteMethodRegistry registry = new TestRemoteMethodRegistry();
protected final CompletableFuture<Trace> firstTrace = new CompletableFuture<>();
protected final Map<Trace, Long> snapshots = new HashMap<>();
protected final CompletableFuture<Void> closed = new CompletableFuture<>();
protected final Map<Trace, TraceRmiTarget> targets = new HashMap<>();
public static class TestRemoteMethodRegistry extends DefaultRemoteMethodRegistry {
@Override
public void add(RemoteMethod method) {
super.add(method);
}
}
public record TestRemoteMethod(String name, ActionName action, 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,
Map<String, RemoteParameter> parameters, SchemaName retType) {
this(name, action, description, parameters, retType, new AsyncPairingQueue<>(),
new AsyncPairingQueue<>());
}
public TestRemoteMethod(String name, ActionName action, String description,
SchemaName retType, RemoteParameter... parameters) {
this(name, action, description, Stream.of(parameters)
.collect(Collectors.toMap(RemoteParameter::name, p -> p)),
retType);
}
@Override
public RemoteAsyncResult invokeAsync(Map<String, Object> arguments) {
argQueue.give().complete(arguments);
DefaultRemoteAsyncResult result = new DefaultRemoteAsyncResult();
retQueue.take().handle(AsyncUtils.copyTo(result));
return result;
}
public Map<String, Object> expect() throws InterruptedException, ExecutionException {
return argQueue.take().get();
}
public void result(Object ret) {
retQueue.give().complete(ret);
}
}
public record TestRemoteParameter(String name, SchemaName type, boolean required,
Object defaultValue, String display, String description) implements RemoteParameter {
@Override
public Object getDefaultValue() {
return defaultValue;
}
}
@Override
public SocketAddress getRemoteAddress() {
return new InetSocketAddress("localhost", 0);
}
@Override
public TestRemoteMethodRegistry getMethods() {
return registry;
}
public void injectTrace(Trace trace) {
firstTrace.complete(trace);
}
public TraceRmiTarget publishTarget(PluginTool tool, Trace trace) {
injectTrace(trace);
TraceRmiTarget target = new TraceRmiTarget(tool, this, trace);
synchronized (targets) {
targets.put(trace, target);
}
DebuggerTargetService targetService = tool.getService(DebuggerTargetService.class);
targetService.publishTarget(target);
return target;
}
@Override
public Trace waitForTrace(long timeoutMillis) throws TimeoutException {
try {
return firstTrace.get(timeoutMillis, TimeUnit.MILLISECONDS);
}
catch (InterruptedException | ExecutionException e) {
throw new AssertionError(e);
}
}
public void setLastSnapshot(Trace trace, long snap) {
synchronized (snapshots) {
snapshots.put(trace, snap);
}
}
@Override
public long getLastSnapshot(Trace trace) {
synchronized (snapshots) {
Long snap = snapshots.get(trace);
return snap == null ? 0 : snap;
}
}
@Override
public void forceCloseTrace(Trace trace) {
TraceRmiTarget target;
synchronized (targets) {
target = targets.remove(trace);
}
DebuggerTargetService targetService =
target.getTool().getService(DebuggerTargetService.class);
targetService.withdrawTarget(target);
}
@Override
public boolean isTarget(Trace trace) {
synchronized (this.targets) {
return targets.containsKey(trace);
}
}
@Override
public void close() throws IOException {
Set<TraceRmiTarget> targets;
synchronized (this.targets) {
targets = new HashSet<>(this.targets.values());
this.targets.clear();
}
for (TraceRmiTarget target : targets) {
DebuggerTargetService targetService =
target.getTool().getService(DebuggerTargetService.class);
targetService.withdrawTarget(target);
}
closed.complete(null);
}
@Override
public boolean isClosed() {
return closed.isDone();
}
@Override
public void waitClosed() {
try {
closed.get();
}
catch (InterruptedException | ExecutionException e) {
throw new AssertionError(e);
}
}
}
@@ -1,3 +1,4 @@
AutoMapSpec
AutoReadMemorySpec
DebuggerBot
DebuggerMappingOpinion
@@ -0,0 +1,60 @@
/* ###
* 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.
*/
import java.util.*;
import ghidra.app.script.GhidraScript;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetThread;
import ghidra.debug.flatapi.FlatDebuggerAPI;
public class MonitorModelEventsScript extends GhidraScript implements FlatDebuggerAPI {
static DebuggerModelListener listener = new DebuggerModelListener() {
@Override
public void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
System.err.println("attributesChanged(%s, removed=%s, added=%s)"
.formatted(object.getJoinedPath("."), removed, added));
}
@Override
public void elementsChanged(TargetObject object, Collection<String> removed,
Map<String, ? extends TargetObject> added) {
System.err.println("elementsChanged(%s, removed=%s, added=%s)"
.formatted(object.getJoinedPath("."), removed, added));
}
@Override
public void event(TargetObject object, TargetThread eventThread, TargetEventType type,
String description, List<Object> parameters) {
System.err.println(
"event(%s, thread=%s, type=%s, desc=%s)".formatted(object.getJoinedPath("."),
eventThread == null ? "<null>" : eventThread.getJoinedPath("."), type,
description));
}
@Override
public void invalidateCacheRequested(TargetObject object) {
System.err.println("invalidateCache(%s)".formatted(object.getJoinedPath(".")));
}
};
@Override
protected void run() throws Exception {
getModelService().getCurrentModel().addModelListener(listener);
}
}
@@ -13,10 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.workflow;
package ghidra.app.plugin.core.debug.disassemble;
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
import ghidra.app.plugin.core.debug.workflow.DisassemblyInjectInfo.CompilerInfo;
import ghidra.app.plugin.core.debug.disassemble.DisassemblyInjectInfo.CompilerInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.lang.Language;
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.workflow;
package ghidra.app.plugin.core.debug.disassemble;
import java.lang.annotation.*;
@@ -296,7 +296,7 @@ public interface DebuggerResources {
boolean DEFAULT_COLOR_INEFF_DIS_BREAKPOINT_COLORING_BACKGROUND = false;
String OPTION_NAME_LOG_BUFFER_LIMIT = "Log Buffer Size";
int DEFAULT_LOG_BUFFER_LIMIT = 100;
int DEFAULT_LOG_BUFFER_LIMIT = 20;
// TODO: Re-assign/name groups
String GROUP_GENERAL = "Dbg1. General";
@@ -878,7 +878,8 @@ public interface DebuggerResources {
static <T> MultiStateActionBuilder<T> builder(Plugin owner) {
String ownerName = owner.getName();
return new MultiStateActionBuilder<T>(NAME, ownerName).description(DESCRIPTION)
return new MultiStateActionBuilder<T>(NAME, ownerName)
.description(DESCRIPTION)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
@@ -0,0 +1,124 @@
/* ###
* 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.app.plugin.core.debug.gui.action;
import java.util.*;
import javax.swing.Icon;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.utils.MiscellaneousUtils;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.ProgramManager;
import ghidra.framework.cmd.BackgroundCommand;
import ghidra.framework.model.DomainObject;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.AutoConfigState.ConfigFieldCodec;
import ghidra.framework.plugintool.PluginTool;
import ghidra.trace.model.Trace;
import ghidra.trace.util.TraceChangeType;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.classfinder.ExtensionPoint;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* An interface for specifying how to automatically map dynamic memory to static memory.
*/
public interface AutoMapSpec extends ExtensionPoint {
class Private {
private final Map<String, AutoMapSpec> specsByName = new TreeMap<>();
private final ChangeListener classListener = this::classesChanged;
private Private() {
ClassSearcher.addChangeListener(classListener);
}
private synchronized void classesChanged(ChangeEvent evt) {
MiscellaneousUtils.collectUniqueInstances(AutoMapSpec.class, specsByName,
AutoMapSpec::getConfigName);
}
}
Private PRIVATE = new Private();
public static class AutoMapSpecConfigFieldCodec implements ConfigFieldCodec<AutoMapSpec> {
@Override
public AutoMapSpec read(SaveState state, String name,
AutoMapSpec current) {
String specName = state.getString(name, null);
return fromConfigName(specName);
}
@Override
public void write(SaveState state, String name, AutoMapSpec value) {
state.putString(name, value.getConfigName());
}
}
static AutoMapSpec fromConfigName(String name) {
synchronized (PRIVATE) {
return PRIVATE.specsByName.get(name);
}
}
static Map<String, AutoMapSpec> allSpecs() {
synchronized (PRIVATE) {
return new TreeMap<>(PRIVATE.specsByName);
}
}
String getConfigName();
String getMenuName();
default Icon getMenuIcon() {
return DebuggerResources.ICON_CONFIG;
}
Collection<TraceChangeType<?, ?>> getChangeTypes();
default String getTaskTitle() {
return getMenuName();
}
default void runTask(PluginTool tool, Trace trace) {
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
ProgramManager programManager = tool.getService(ProgramManager.class);
if (mappingService == null || programManager == null) {
return;
}
BackgroundCommand cmd = new BackgroundCommand(getTaskTitle(), true, true, false) {
@Override
public boolean applyTo(DomainObject obj, TaskMonitor monitor) {
try {
performMapping(mappingService, trace, programManager, monitor);
return true;
}
catch (CancelledException e) {
return false;
}
}
};
tool.executeBackgroundCommand(cmd, trace);
}
void performMapping(DebuggerStaticMappingService mappingService, Trace trace,
ProgramManager programManager, TaskMonitor monitor) throws CancelledException;
}
@@ -15,7 +15,8 @@
*/
package ghidra.app.plugin.core.debug.gui.action;
import java.util.*;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import javax.swing.Icon;
@@ -31,9 +32,12 @@ import ghidra.program.model.address.AddressSetView;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.classfinder.ExtensionPoint;
/**
* An interface for specifying how to automatically read target memory.
*/
public interface AutoReadMemorySpec extends ExtensionPoint {
class Private {
private final Map<String, AutoReadMemorySpec> specsByName = new HashMap<>();
private final Map<String, AutoReadMemorySpec> specsByName = new TreeMap<>();
private final ChangeListener classListener = this::classesChanged;
private Private() {
@@ -86,13 +90,14 @@ public interface AutoReadMemorySpec extends ExtensionPoint {
*
* <p>
* Note, the implementation should perform all the error handling. The returned future is for
* follow-up purposes only, and should always complete normally.
* follow-up purposes only, and should always complete normally. It should complete with true if
* any memory was actually loaded. Otherwise, it should complete with false.
*
* @param tool the tool containing the provider
* @param coordinates the provider's current coordinates
* @param visible the provider's visible addresses
* @return a future that completes when the memory has been read
*/
CompletableFuture<?> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
CompletableFuture<Boolean> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
AddressSetView visible);
}
@@ -13,17 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.workflow;
package ghidra.app.plugin.core.debug.gui.action;
import java.util.*;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.ProgramManager;
import ghidra.debug.api.modules.MapProposal;
import ghidra.debug.api.modules.ModuleMapProposal;
import ghidra.debug.api.modules.ModuleMapProposal.ModuleMapEntry;
import ghidra.debug.api.workflow.DebuggerBotInfo;
import ghidra.framework.options.annotation.HelpInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Trace;
import ghidra.trace.model.Trace.TraceMemoryRegionChangeType;
@@ -32,30 +30,33 @@ import ghidra.trace.util.TraceChangeType;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@DebuggerBotInfo(
description = "Map modules to open programs",
details = "Monitors open traces and programs, attempting to map modules by \"best\" match.",
help = @HelpInfo(anchor = "map_modules"),
enabledByDefault = true)
public class MapModulesDebuggerBot extends AbstractMapDebuggerBot {
public class ByModuleAutoMapSpec implements AutoMapSpec {
public static final String CONFIG_NAME = "1_MAP_BY_MODULE";
@Override
protected Collection<TraceChangeType<?, ?>> getChangeTypes() {
public String getConfigName() {
return CONFIG_NAME;
}
@Override
public String getMenuName() {
return "Auto-Map by Module";
}
@Override
public Collection<TraceChangeType<?, ?>> getChangeTypes() {
return List.of(TraceModuleChangeType.ADDED, TraceModuleChangeType.CHANGED,
TraceMemoryRegionChangeType.ADDED, TraceMemoryRegionChangeType.CHANGED);
}
@Override
protected void doAnalysis(PluginTool tool, Trace trace, Set<Program> programs,
TaskMonitor monitor) throws CancelledException {
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
if (mappingService != null) {
Map<?, ModuleMapProposal> maps = mappingService
.proposeModuleMaps(trace.getModuleManager().getAllModules(), programs);
Collection<ModuleMapEntry> entries = MapProposal.flatten(maps.values());
entries = MapProposal.removeOverlapping(entries);
mappingService.addModuleMappings(entries, monitor, false);
}
public void performMapping(DebuggerStaticMappingService mappingService, Trace trace,
ProgramManager programManager, TaskMonitor monitor) throws CancelledException {
List<Program> programs = Arrays.asList(programManager.getAllOpenPrograms());
Map<?, ModuleMapProposal> maps = mappingService
.proposeModuleMaps(trace.getModuleManager().getAllModules(), programs);
Collection<ModuleMapEntry> entries = MapProposal.flatten(maps.values());
entries = MapProposal.removeOverlapping(entries);
mappingService.addModuleMappings(entries, monitor, false);
}
}
@@ -13,17 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.workflow;
package ghidra.app.plugin.core.debug.gui.action;
import java.util.*;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.ProgramManager;
import ghidra.debug.api.modules.MapProposal;
import ghidra.debug.api.modules.RegionMapProposal;
import ghidra.debug.api.modules.RegionMapProposal.RegionMapEntry;
import ghidra.debug.api.workflow.DebuggerBotInfo;
import ghidra.framework.options.annotation.HelpInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Trace;
import ghidra.trace.model.Trace.TraceMemoryRegionChangeType;
@@ -31,29 +29,32 @@ import ghidra.trace.util.TraceChangeType;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@DebuggerBotInfo(
description = "Map regions to open programs",
details = "Monitors open traces and programs, attempting to map regions by \"best\" match.",
help = @HelpInfo(anchor = "map_regions"),
enabledByDefault = false)
public class MapRegionsDebuggerBot extends AbstractMapDebuggerBot {
public class ByRegionAutoMapSpec implements AutoMapSpec {
public static final String CONFIG_NAME = "1_MAP_BY_REGION";
@Override
protected Collection<TraceChangeType<?, ?>> getChangeTypes() {
public String getConfigName() {
return CONFIG_NAME;
}
@Override
public String getMenuName() {
return "Auto-Map by Region";
}
@Override
public Collection<TraceChangeType<?, ?>> getChangeTypes() {
return List.of(TraceMemoryRegionChangeType.ADDED);
}
@Override
protected void doAnalysis(PluginTool tool, Trace trace, Set<Program> programs,
TaskMonitor monitor) throws CancelledException {
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
if (mappingService != null) {
Map<?, RegionMapProposal> maps = mappingService
.proposeRegionMaps(trace.getMemoryManager().getAllRegions(), programs);
Collection<RegionMapEntry> entries = MapProposal.flatten(maps.values());
entries = MapProposal.removeOverlapping(entries);
mappingService.addRegionMappings(entries, monitor, false);
}
public void performMapping(DebuggerStaticMappingService mappingService, Trace trace,
ProgramManager programManager, TaskMonitor monitor) throws CancelledException {
List<Program> programs = Arrays.asList(programManager.getAllOpenPrograms());
Map<?, RegionMapProposal> maps = mappingService
.proposeRegionMaps(trace.getMemoryManager().getAllRegions(), programs);
Collection<RegionMapEntry> entries = MapProposal.flatten(maps.values());
entries = MapProposal.removeOverlapping(entries);
mappingService.addRegionMappings(entries, monitor, false);
}
}
@@ -13,17 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.workflow;
package ghidra.app.plugin.core.debug.gui.action;
import java.util.*;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.ProgramManager;
import ghidra.debug.api.modules.MapProposal;
import ghidra.debug.api.modules.SectionMapProposal;
import ghidra.debug.api.modules.SectionMapProposal.SectionMapEntry;
import ghidra.debug.api.workflow.DebuggerBotInfo;
import ghidra.framework.options.annotation.HelpInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Trace;
import ghidra.trace.model.Trace.TraceSectionChangeType;
@@ -31,29 +29,32 @@ import ghidra.trace.util.TraceChangeType;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@DebuggerBotInfo(
description = "Map sections to open programs",
details = "Monitors open traces and programs, attempting to map sections by \"best\" match.",
help = @HelpInfo(anchor = "map_sections"),
enabledByDefault = false)
public class MapSectionsDebuggerBot extends AbstractMapDebuggerBot {
public class BySectionAutoMapSpec implements AutoMapSpec {
public static final String CONFIG_NAME = "1_MAP_BY_SECTION";
@Override
protected Collection<TraceChangeType<?, ?>> getChangeTypes() {
public String getConfigName() {
return CONFIG_NAME;
}
@Override
public String getMenuName() {
return "Auto-Map by Section";
}
@Override
public Collection<TraceChangeType<?, ?>> getChangeTypes() {
return List.of(TraceSectionChangeType.ADDED);
}
@Override
protected void doAnalysis(PluginTool tool, Trace trace, Set<Program> programs,
TaskMonitor monitor) throws CancelledException {
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
if (mappingService != null) {
Map<?, SectionMapProposal> maps = mappingService
.proposeSectionMaps(trace.getModuleManager().getAllModules(), programs);
Collection<SectionMapEntry> entries = MapProposal.flatten(maps.values());
entries = MapProposal.removeOverlapping(entries);
mappingService.addSectionMappings(entries, monitor, false);
}
public void performMapping(DebuggerStaticMappingService mappingService, Trace trace,
ProgramManager programManager, TaskMonitor monitor) throws CancelledException {
List<Program> programs = Arrays.asList(programManager.getAllOpenPrograms());
Map<?, SectionMapProposal> maps = mappingService
.proposeSectionMaps(trace.getModuleManager().getAllModules(), programs);
Collection<SectionMapEntry> entries = MapProposal.flatten(maps.values());
entries = MapProposal.removeOverlapping(entries);
mappingService.addSectionMappings(entries, monitor, false);
}
}
@@ -36,8 +36,7 @@ import ghidra.framework.model.DomainObject;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.annotation.AutoConfigStateField;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.address.*;
import ghidra.trace.model.*;
import ghidra.trace.model.Trace.TraceMemoryStateChangeType;
import ghidra.trace.model.Trace.TraceSnapshotChangeType;
@@ -79,6 +78,7 @@ public abstract class DebuggerReadsMemoryTrait {
target.invalidateMemoryCaches();
try {
target.readMemory(sel, monitor);
memoryWasRead(sel);
}
catch (CancelledException e) {
return false;
@@ -219,7 +219,12 @@ public abstract class DebuggerReadsMemoryTrait {
if (!isConsistent()) {
return;
}
autoSpec.readMemory(tool, current, visible).exceptionally(ex -> {
AddressSet visible = new AddressSet(this.visible);
autoSpec.readMemory(tool, current, visible).thenAccept(b -> {
if (b) {
memoryWasRead(visible);
}
}).exceptionally(ex -> {
Msg.error(this, "Could not auto-read memory: " + ex);
return null;
});
@@ -286,4 +291,8 @@ public abstract class DebuggerReadsMemoryTrait {
protected abstract AddressSetView getSelection();
protected abstract void repaintPanel();
protected void memoryWasRead(AddressSetView read) {
// Extension point
}
}
@@ -26,7 +26,6 @@ import ghidra.app.plugin.core.debug.service.emulation.ProgramEmulationUtils;
import ghidra.app.plugin.core.debug.service.model.record.RecorderUtils;
import ghidra.app.plugin.core.debug.utils.AbstractMappedMemoryBytesVisitor;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.async.AsyncUtils;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.*;
@@ -37,7 +36,7 @@ import ghidra.trace.model.memory.TraceMemoryManager;
import ghidra.trace.model.memory.TraceMemoryState;
public class LoadEmulatorAutoReadMemorySpec implements AutoReadMemorySpec {
public static final String CONFIG_NAME = "LOAD_EMULATOR";
public static final String CONFIG_NAME = "2_LOAD_EMULATOR";
@Override
public String getConfigName() {
@@ -55,18 +54,18 @@ public class LoadEmulatorAutoReadMemorySpec implements AutoReadMemorySpec {
}
@Override
public CompletableFuture<?> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
public CompletableFuture<Boolean> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
AddressSetView visible) {
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
if (mappingService == null) {
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
Trace trace = coordinates.getTrace();
if (trace == null || coordinates.isAlive() ||
!ProgramEmulationUtils.isEmulatedProgram(trace)) {
// Never interfere with a live target
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
TraceMemoryManager mm = trace.getMemoryManager();
AddressSet toRead = new AddressSet(RecorderUtils.INSTANCE.quantize(12, visible));
@@ -80,7 +79,7 @@ public class LoadEmulatorAutoReadMemorySpec implements AutoReadMemorySpec {
}
if (toRead.isEmpty()) {
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
long snap = coordinates.getSnap();
@@ -94,7 +93,7 @@ public class LoadEmulatorAutoReadMemorySpec implements AutoReadMemorySpec {
mm.putBytes(snap, hostAddr, buf);
}
}.visit(trace, snap, toRead);
return AsyncUtils.nil();
return CompletableFuture.completedFuture(true);
}
catch (MemoryAccessException e) {
throw new AssertionError(e);
@@ -0,0 +1,56 @@
/* ###
* 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.app.plugin.core.debug.gui.action;
import java.util.Collection;
import java.util.List;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.ProgramManager;
import ghidra.framework.plugintool.PluginTool;
import ghidra.trace.model.Trace;
import ghidra.trace.util.TraceChangeType;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
public class NoneAutoMapSpec implements AutoMapSpec {
public static final String CONFIG_NAME = "0_MAP_NONE";
@Override
public String getConfigName() {
return CONFIG_NAME;
}
@Override
public String getMenuName() {
return "Do Not Auto-Map";
}
@Override
public Collection<TraceChangeType<?, ?>> getChangeTypes() {
return List.of();
}
@Override
public void runTask(PluginTool tool, Trace trace) {
// Don't bother launching a task that does nothing
}
@Override
public void performMapping(DebuggerStaticMappingService mappingService, Trace trace,
ProgramManager programManager, TaskMonitor monitor) throws CancelledException {
}
}
@@ -20,13 +20,12 @@ import java.util.concurrent.CompletableFuture;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.AutoReadMemoryAction;
import ghidra.async.AsyncUtils;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.AddressSetView;
public class NoneAutoReadMemorySpec implements AutoReadMemorySpec {
public static final String CONFIG_NAME = "READ_NONE";
public static final String CONFIG_NAME = "0_READ_NONE";
@Override
public String getConfigName() {
@@ -44,8 +43,8 @@ public class NoneAutoReadMemorySpec implements AutoReadMemorySpec {
}
@Override
public CompletableFuture<Void> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
public CompletableFuture<Boolean> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
AddressSetView visible) {
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
}
@@ -90,4 +90,9 @@ public enum NoneLocationTrackingSpec implements LocationTrackingSpec, LocationTr
public boolean affectedByStackChange(TraceStack stack, DebuggerCoordinates coordinates) {
return false;
}
@Override
public boolean shouldDisassemble() {
return false;
}
}
@@ -0,0 +1,64 @@
/* ###
* 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.app.plugin.core.debug.gui.action;
import java.util.Collection;
import java.util.List;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.ProgramManager;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.modules.TraceConflictedMappingException;
import ghidra.trace.util.TraceChangeType;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
public class OneToOneAutoMapSpec implements AutoMapSpec {
public static final String CONFIG_NAME = "2_MAP_ONE_TO_ONE";
@Override
public String getConfigName() {
return CONFIG_NAME;
}
@Override
public String getMenuName() {
return "Auto-Map Identically (1-to-1)";
}
@Override
public Collection<TraceChangeType<?, ?>> getChangeTypes() {
return List.of();
}
@Override
public void performMapping(DebuggerStaticMappingService mappingService, Trace trace,
ProgramManager programManager, TaskMonitor monitor) throws CancelledException {
Program program = programManager.getCurrentProgram();
if (program == null) {
return;
}
try {
mappingService.addIdentityMapping(trace, program,
Lifespan.nowOn(trace.getProgramView().getSnap()), false);
}
catch (TraceConflictedMappingException e) {
// aww well
}
}
}
@@ -61,4 +61,9 @@ public enum PCByRegisterLocationTrackingSpec implements RegisterLocationTracking
public AddressSpace computeDefaultAddressSpace(DebuggerCoordinates coordinates) {
return coordinates.getPlatform().getLanguage().getDefaultSpace();
}
@Override
public boolean shouldDisassemble() {
return true;
}
}
@@ -125,4 +125,9 @@ public enum PCByStackLocationTrackingSpec implements LocationTrackingSpec, Locat
DebuggerCoordinates coordinates) {
return false;
}
@Override
public boolean shouldDisassemble() {
return true;
}
}
@@ -100,4 +100,9 @@ public enum PCLocationTrackingSpec implements LocationTrackingSpec, LocationTrac
DebuggerCoordinates coordinates) {
return BY_REG.affectedByBytesChange(space, range, coordinates);
}
@Override
public boolean shouldDisassemble() {
return true;
}
}
@@ -61,4 +61,9 @@ public enum SPLocationTrackingSpec implements RegisterLocationTrackingSpec {
public AddressSpace computeDefaultAddressSpace(DebuggerCoordinates coordinates) {
return coordinates.getTrace().getBaseLanguage().getDefaultDataSpace();
}
@Override
public boolean shouldDisassemble() {
return false;
}
}
@@ -20,7 +20,6 @@ import java.util.concurrent.CompletableFuture;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.AutoReadMemoryAction;
import ghidra.async.AsyncUtils;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginTool;
@@ -31,7 +30,7 @@ import ghidra.trace.model.memory.TraceMemoryState;
import ghidra.util.task.TaskMonitor;
public class VisibleAutoReadMemorySpec implements AutoReadMemorySpec {
public static final String CONFIG_NAME = "READ_VISIBLE";
public static final String CONFIG_NAME = "1_READ_VISIBLE";
@Override
public String getConfigName() {
@@ -49,10 +48,10 @@ public class VisibleAutoReadMemorySpec implements AutoReadMemorySpec {
}
@Override
public CompletableFuture<?> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
public CompletableFuture<Boolean> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
AddressSetView visible) {
if (!coordinates.isAliveAndReadsPresent()) {
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
Target target = coordinates.getTarget();
TraceMemoryManager mm = coordinates.getTrace().getMemoryManager();
@@ -61,9 +60,9 @@ public class VisibleAutoReadMemorySpec implements AutoReadMemorySpec {
AddressSet toRead = visible.subtract(alreadyKnown);
if (toRead.isEmpty()) {
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
return target.readMemoryAsync(toRead, TaskMonitor.DUMMY);
return target.readMemoryAsync(toRead, TaskMonitor.DUMMY).thenApply(__ -> true);
}
}

Some files were not shown because too many files have changed in this diff Show More