mirror of
https://github.com/NationalSecurityAgency/ghidra
synced 2024-08-27 21:10:26 +00:00
GP-3857: Port most Debugger components to TraceRmi.
This commit is contained in:
parent
7e4d2bcfaa
commit
fd4380c07a
|
@ -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;
|
||||
}
|
|
@ -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 → Terminals →
|
||||
...</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()) + " → <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
|
||||
|
|
|
@ -17,10 +17,8 @@ package ghidra.app.plugin.core.debug.service.rmi.trace;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
@ -28,7 +26,9 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
|
|||
import docking.ActionContext;
|
||||
import ghidra.app.context.ProgramLocationActionContext;
|
||||
import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext;
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.RemoteMethodInvocationDialog;
|
||||
import ghidra.app.plugin.core.debug.service.target.AbstractTarget;
|
||||
import ghidra.app.services.DebuggerConsoleService;
|
||||
import ghidra.app.services.DebuggerTraceManagerService;
|
||||
import ghidra.async.*;
|
||||
import ghidra.dbg.target.*;
|
||||
|
@ -49,7 +49,9 @@ import ghidra.program.model.lang.RegisterValue;
|
|||
import ghidra.trace.model.Lifespan;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.breakpoint.*;
|
||||
import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
|
||||
import ghidra.trace.model.guest.TracePlatform;
|
||||
import ghidra.trace.model.memory.TraceObjectMemoryRegion;
|
||||
import ghidra.trace.model.stack.*;
|
||||
import ghidra.trace.model.target.*;
|
||||
import ghidra.trace.model.thread.TraceObjectThread;
|
||||
|
@ -63,10 +65,12 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
private static final String BREAK_READ = "breakRead";
|
||||
private static final String BREAK_WRITE = "breakWrite";
|
||||
private static final String BREAK_ACCESS = "breakAccess";
|
||||
|
||||
private final TraceRmiConnection connection;
|
||||
private final Trace trace;
|
||||
|
||||
private final Matches matches = new Matches();
|
||||
private final RequestCaches requestCaches = new RequestCaches();
|
||||
private final Set<TraceBreakpointKind> supportedBreakpointKinds;
|
||||
|
||||
public TraceRmiTarget(PluginTool tool, TraceRmiConnection connection, Trace trace) {
|
||||
|
@ -78,7 +82,7 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return !connection.isClosed();
|
||||
return !connection.isClosed() && connection.isTarget(trace);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -88,7 +92,12 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
|
||||
@Override
|
||||
public long getSnap() {
|
||||
return connection.getLastSnapshot(trace);
|
||||
try {
|
||||
return connection.getLastSnapshot(trace);
|
||||
}
|
||||
catch (NoSuchElementException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -122,52 +131,72 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
.orElse(null);
|
||||
}
|
||||
|
||||
protected TraceObject findObject(ActionContext context) {
|
||||
if (context instanceof DebuggerObjectActionContext ctx) {
|
||||
List<TraceObjectValue> values = ctx.getObjectValues();
|
||||
if (values.size() == 1) {
|
||||
TraceObjectValue ov = values.get(0);
|
||||
if (ov.isObject()) {
|
||||
return ov.getChild();
|
||||
protected TraceObject findObject(ActionContext context, boolean allowContextObject,
|
||||
boolean allowCoordsObject) {
|
||||
if (allowContextObject) {
|
||||
if (context instanceof DebuggerObjectActionContext ctx) {
|
||||
List<TraceObjectValue> values = ctx.getObjectValues();
|
||||
if (values.size() == 1) {
|
||||
TraceObjectValue ov = values.get(0);
|
||||
if (ov.isObject()) {
|
||||
return ov.getChild();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DebuggerTraceManagerService traceManager =
|
||||
tool.getService(DebuggerTraceManagerService.class);
|
||||
if (traceManager != null) {
|
||||
return traceManager.getCurrentObject();
|
||||
if (allowCoordsObject) {
|
||||
DebuggerTraceManagerService traceManager =
|
||||
tool.getService(DebuggerTraceManagerService.class);
|
||||
if (traceManager == null) {
|
||||
return null;
|
||||
}
|
||||
return traceManager.getCurrentFor(trace).getObject();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Object findArgumentForSchema(ActionContext context, TargetObjectSchema schema) {
|
||||
protected Object findArgumentForSchema(ActionContext context, TargetObjectSchema schema,
|
||||
boolean allowContextObject, boolean allowCoordsObject, boolean allowSuitableObject) {
|
||||
if (schema instanceof EnumerableTargetObjectSchema prim) {
|
||||
return switch (prim) {
|
||||
case OBJECT -> findObject(context);
|
||||
case OBJECT -> findObject(context, allowContextObject, allowCoordsObject);
|
||||
case ADDRESS -> findAddress(context);
|
||||
case RANGE -> findRange(context);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
TraceObject object = findObject(context);
|
||||
TraceObject object = findObject(context, allowContextObject, allowCoordsObject);
|
||||
if (object == null) {
|
||||
return null;
|
||||
}
|
||||
return object.querySuitableSchema(schema);
|
||||
if (allowSuitableObject) {
|
||||
return object.querySuitableSchema(schema);
|
||||
}
|
||||
if (object.getTargetSchema() == schema) {
|
||||
return object;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private enum Missing {
|
||||
MISSING; // The argument requires a prompt
|
||||
}
|
||||
|
||||
protected Object findArgument(RemoteParameter parameter, ActionContext context) {
|
||||
protected Object findArgument(RemoteParameter parameter, ActionContext context,
|
||||
boolean allowContextObject, boolean allowCoordsObject, boolean allowSuitableObject) {
|
||||
SchemaName type = parameter.type();
|
||||
TargetObjectSchema schema = getSchemaContext().getSchema(type);
|
||||
SchemaContext ctx = getSchemaContext();
|
||||
if (ctx == null) {
|
||||
Msg.trace(this, "No root schema, yet: " + trace);
|
||||
return null;
|
||||
}
|
||||
TargetObjectSchema schema = ctx.getSchema(type);
|
||||
if (schema == null) {
|
||||
Msg.error(this, "Schema " + type + " not in trace! " + trace);
|
||||
return null;
|
||||
}
|
||||
Object arg = findArgumentForSchema(context, schema);
|
||||
Object arg = findArgumentForSchema(context, schema, allowContextObject, allowCoordsObject,
|
||||
allowSuitableObject);
|
||||
if (arg != null) {
|
||||
return arg;
|
||||
}
|
||||
|
@ -177,27 +206,46 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
return Missing.MISSING;
|
||||
}
|
||||
|
||||
protected Map<String, Object> collectArguments(RemoteMethod method, ActionContext context) {
|
||||
return method.parameters()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.collect(
|
||||
Collectors.toMap(Entry::getKey, e -> findArgument(e.getValue(), context)));
|
||||
protected Map<String, Object> collectArguments(RemoteMethod method, ActionContext context,
|
||||
boolean allowContextObject, boolean allowCoordsObject, boolean allowSuitableObject) {
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
for (RemoteParameter param : method.parameters().values()) {
|
||||
Object found = findArgument(param, context, allowContextObject, allowCoordsObject,
|
||||
allowSuitableObject);
|
||||
if (found != null) {
|
||||
args.put(param.name(), found);
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
private TargetExecutionState getStateOf(TraceObject object) {
|
||||
return object.getExecutionState(getSnap());
|
||||
try {
|
||||
return object.getExecutionState(getSnap());
|
||||
}
|
||||
catch (NoSuchElementException e) {
|
||||
return TargetExecutionState.TERMINATED;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean stateOrNull(TraceObject object,
|
||||
private boolean whenState(TraceObject object,
|
||||
Predicate<TargetExecutionState> predicate) {
|
||||
TargetExecutionState state = getStateOf(object);
|
||||
return state == null || predicate.test(state);
|
||||
try {
|
||||
TargetExecutionState state = getStateOf(object);
|
||||
return state == null || predicate.test(state);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Could not get state: " + e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected BooleanSupplier chooseEnabler(RemoteMethod method, Map<String, Object> args) {
|
||||
ActionName name = method.action();
|
||||
SchemaContext ctx = getSchemaContext();
|
||||
if (ctx == null) {
|
||||
return () -> true;
|
||||
}
|
||||
RemoteParameter firstParam = method.parameters()
|
||||
.values()
|
||||
.stream()
|
||||
|
@ -207,7 +255,12 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
if (firstParam == null) {
|
||||
return () -> true;
|
||||
}
|
||||
TraceObject firstArg = (TraceObject) args.get(firstParam.name());
|
||||
Object firstArg = args.get(firstParam.name());
|
||||
if (firstArg == null || firstArg == Missing.MISSING) {
|
||||
Msg.trace(this, "MISSING first argument for " + method + "(" + firstParam + ")");
|
||||
return () -> false;
|
||||
}
|
||||
TraceObject obj = (TraceObject) firstArg;
|
||||
if (ActionName.RESUME.equals(name) ||
|
||||
ActionName.STEP_BACK.equals(name) ||
|
||||
ActionName.STEP_EXT.equals(name) ||
|
||||
|
@ -215,29 +268,74 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
ActionName.STEP_OUT.equals(name) ||
|
||||
ActionName.STEP_OVER.equals(name) ||
|
||||
ActionName.STEP_SKIP.equals(name)) {
|
||||
return () -> stateOrNull(firstArg, TargetExecutionState::isStopped);
|
||||
return () -> whenState(obj, state -> state != null && state.isStopped());
|
||||
}
|
||||
else if (ActionName.INTERRUPT.equals(name)) {
|
||||
return () -> stateOrNull(firstArg, TargetExecutionState::isRunning);
|
||||
return () -> whenState(obj, state -> state == null || state.isRunning());
|
||||
}
|
||||
else if (ActionName.KILL.equals(name)) {
|
||||
return () -> stateOrNull(firstArg, TargetExecutionState::isAlive);
|
||||
return () -> whenState(obj, state -> state == null || state.isAlive());
|
||||
}
|
||||
return () -> true;
|
||||
}
|
||||
|
||||
protected ActionEntry createEntry(RemoteMethod method, ActionContext context) {
|
||||
Map<String, Object> args = collectArguments(method, context);
|
||||
private Map<String, Object> promptArgs(RemoteMethod method, Map<String, Object> defaults) {
|
||||
SchemaContext ctx = getSchemaContext();
|
||||
RemoteMethodInvocationDialog dialog = new RemoteMethodInvocationDialog(tool,
|
||||
method.name(), method.name(), null);
|
||||
while (true) {
|
||||
for (RemoteParameter param : method.parameters().values()) {
|
||||
Object val = defaults.get(param.name());
|
||||
if (val != null) {
|
||||
Class<?> type = ctx.getSchema(param.type()).getType();
|
||||
dialog.setMemorizedArgument(param.name(), type.asSubclass(Object.class),
|
||||
val);
|
||||
}
|
||||
}
|
||||
Map<String, Object> args = dialog.promptArguments(ctx, method.parameters(), defaults);
|
||||
if (args == null) {
|
||||
// Cancelled
|
||||
return null;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<?> invokeMethod(boolean prompt, RemoteMethod method,
|
||||
Map<String, Object> arguments) {
|
||||
Map<String, Object> chosenArgs;
|
||||
if (prompt) {
|
||||
chosenArgs = promptArgs(method, arguments);
|
||||
}
|
||||
else {
|
||||
chosenArgs = arguments;
|
||||
}
|
||||
return method.invokeAsync(chosenArgs).thenAccept(result -> {
|
||||
DebuggerConsoleService consoleService =
|
||||
tool.getService(DebuggerConsoleService.class);
|
||||
Class<?> retType = getSchemaContext().getSchema(method.retType()).getType();
|
||||
if (consoleService != null && retType != Void.class && retType != Object.class) {
|
||||
consoleService.log(null, method.name() + " returned " + result);
|
||||
}
|
||||
}).toCompletableFuture();
|
||||
}
|
||||
|
||||
protected ActionEntry createEntry(RemoteMethod method, ActionContext context,
|
||||
boolean allowContextObject, boolean allowCoordsObject, boolean allowSuitableObject) {
|
||||
Map<String, Object> args = collectArguments(method, context, allowContextObject,
|
||||
allowCoordsObject, allowSuitableObject);
|
||||
boolean requiresPrompt = args.values().contains(Missing.MISSING);
|
||||
return new ActionEntry(method.name(), method.action(), method.description(), requiresPrompt,
|
||||
chooseEnabler(method, args), () -> method.invokeAsync(args).toCompletableFuture());
|
||||
chooseEnabler(method, args), prompt -> invokeMethod(prompt, method, args));
|
||||
}
|
||||
|
||||
protected Map<String, ActionEntry> collectFromMethods(Collection<RemoteMethod> methods,
|
||||
ActionContext context) {
|
||||
ActionContext context, boolean allowContextObject, boolean allowCoordsObject,
|
||||
boolean allowSuitableObject) {
|
||||
Map<String, ActionEntry> result = new HashMap<>();
|
||||
for (RemoteMethod m : methods) {
|
||||
result.put(m.name(), createEntry(m, context));
|
||||
result.put(m.name(), createEntry(m, context, allowContextObject, allowCoordsObject,
|
||||
allowSuitableObject));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -246,7 +344,15 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
return method.parameters()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(p -> ctx.getSchema(p.type()).getType() == Address.class)
|
||||
.filter(p -> {
|
||||
TargetObjectSchema schema = ctx.getSchemaOrNull(p.type());
|
||||
if (schema == null) {
|
||||
Msg.error(this,
|
||||
"Method " + method + " refers to invalid schema name: " + p.type());
|
||||
return false;
|
||||
}
|
||||
return schema.getType() == Address.class;
|
||||
})
|
||||
.count() == 1;
|
||||
}
|
||||
|
||||
|
@ -258,18 +364,20 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
if (!isAddressMethod(m, ctx)) {
|
||||
continue;
|
||||
}
|
||||
result.put(m.name(), createEntry(m, context));
|
||||
result.put(m.name(), createEntry(m, context, true, true, true));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, ActionEntry> collectAllActions(ActionContext context) {
|
||||
return collectFromMethods(connection.getMethods().all().values(), context);
|
||||
return collectFromMethods(connection.getMethods().all().values(), context, true, false,
|
||||
false);
|
||||
}
|
||||
|
||||
protected Map<String, ActionEntry> collectByName(ActionName name, ActionContext context) {
|
||||
return collectFromMethods(connection.getMethods().getByAction(name), context);
|
||||
return collectFromMethods(connection.getMethods().getByAction(name), context, false, true,
|
||||
true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -311,7 +419,7 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
public boolean isSupportsFocus() {
|
||||
TargetObjectSchema schema = trace.getObjectManager().getRootSchema();
|
||||
if (schema == null) {
|
||||
Msg.warn(this, "Checked for focus support before root schema is available");
|
||||
Msg.trace(this, "Checked for focus support before root schema is available");
|
||||
return false;
|
||||
}
|
||||
return schema
|
||||
|
@ -370,10 +478,17 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
}
|
||||
}
|
||||
|
||||
protected static boolean typeMatches(RemoteParameter param, SchemaContext ctx, Class<?> type) {
|
||||
TargetObjectSchema sch = ctx.getSchema(param.type());
|
||||
protected static boolean typeMatches(RemoteMethod method, RemoteParameter param,
|
||||
SchemaContext ctx, Class<?> type) {
|
||||
TargetObjectSchema sch = ctx.getSchemaOrNull(param.type());
|
||||
if (sch == null) {
|
||||
throw new RuntimeException(
|
||||
"The parameter '%s' of method '%s' refers to a non-existent schema '%s'"
|
||||
.formatted(param.name(), method.name(), param.type()));
|
||||
}
|
||||
if (type == TargetObject.class) {
|
||||
return sch.getType() == type;
|
||||
// The method cannot impose any further restriction. It must accept any object.
|
||||
return sch == EnumerableTargetObjectSchema.OBJECT;
|
||||
}
|
||||
else if (TargetObject.class.isAssignableFrom(type)) {
|
||||
return sch.getInterfaces().contains(type);
|
||||
|
@ -389,13 +504,28 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
RemoteParameter find(RemoteMethod method, SchemaContext ctx);
|
||||
}
|
||||
|
||||
record SchemaParamSpec(String name, SchemaName schema) implements ParamSpec {
|
||||
@Override
|
||||
public RemoteParameter find(RemoteMethod method, SchemaContext ctx) {
|
||||
List<RemoteParameter> withType = method.parameters()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(p -> schema.equals(p.type()))
|
||||
.toList();
|
||||
if (withType.size() != 1) {
|
||||
return null;
|
||||
}
|
||||
return withType.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
record TypeParamSpec(String name, Class<?> type) implements ParamSpec {
|
||||
@Override
|
||||
public RemoteParameter find(RemoteMethod method, SchemaContext ctx) {
|
||||
List<RemoteParameter> withType = method.parameters()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(p -> typeMatches(p, ctx, type))
|
||||
.filter(p -> typeMatches(method, p, ctx, type))
|
||||
.toList();
|
||||
if (withType.size() != 1) {
|
||||
return null;
|
||||
|
@ -408,16 +538,15 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
@Override
|
||||
public RemoteParameter find(RemoteMethod method, SchemaContext ctx) {
|
||||
RemoteParameter param = method.parameters().get(name);
|
||||
if (typeMatches(param, ctx, type)) {
|
||||
if (param != null && typeMatches(method, param, ctx, type)) {
|
||||
return param;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
protected static <T extends MethodMatcher> List<T> matchers(T... list) {
|
||||
List<T> result = List.of(list);
|
||||
protected static <T extends MethodMatcher> List<T> matchers(List<T> list) {
|
||||
List<T> result = new ArrayList<>(list);
|
||||
result.sort(Comparator.comparing(MethodMatcher::score).reversed());
|
||||
if (result.isEmpty()) {
|
||||
throw new AssertionError("empty matchers list?");
|
||||
|
@ -429,33 +558,60 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
throw new AssertionError("duplicate scores: " + curScore);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
protected static <T extends MethodMatcher> List<T> matchers(T... list) {
|
||||
return matchers(Arrays.asList(list));
|
||||
}
|
||||
|
||||
record ActivateMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
|
||||
static final ActivateMatcher HAS_FOCUS_TIME = new ActivateMatcher(3, List.of(
|
||||
new TypeParamSpec("focus", TargetObject.class),
|
||||
new TypeParamSpec("time", String.class)));
|
||||
static final ActivateMatcher HAS_FOCUS_SNAP = new ActivateMatcher(2, List.of(
|
||||
new TypeParamSpec("focus", TargetObject.class),
|
||||
new TypeParamSpec("snap", Long.class)));
|
||||
static final ActivateMatcher HAS_FOCUS = new ActivateMatcher(1, List.of(
|
||||
new TypeParamSpec("focus", TargetObject.class)));
|
||||
static final List<ActivateMatcher> ALL =
|
||||
matchers(HAS_FOCUS_TIME, HAS_FOCUS_SNAP, HAS_FOCUS);
|
||||
static List<ActivateMatcher> makeAllFor(int addScore, ParamSpec focusSpec) {
|
||||
ActivateMatcher hasFocusTime = new ActivateMatcher(addScore + 3, List.of(
|
||||
focusSpec,
|
||||
new TypeParamSpec("time", String.class)));
|
||||
ActivateMatcher hasFocusSnap = new ActivateMatcher(addScore + 2, List.of(
|
||||
focusSpec,
|
||||
new TypeParamSpec("snap", Long.class)));
|
||||
ActivateMatcher hasFocus = new ActivateMatcher(addScore + 1, List.of(
|
||||
focusSpec));
|
||||
return matchers(hasFocusTime, hasFocusSnap, hasFocus);
|
||||
}
|
||||
|
||||
static List<ActivateMatcher> makeBySpecificity(TargetObjectSchema rootSchema,
|
||||
TraceObjectKeyPath path) {
|
||||
List<ActivateMatcher> result = new ArrayList<>();
|
||||
List<String> keyList = path.getKeyList();
|
||||
result.addAll(makeAllFor((keyList.size() + 1) * 3,
|
||||
new TypeParamSpec("focus", TargetObject.class)));
|
||||
List<TargetObjectSchema> schemas = rootSchema.getSuccessorSchemas(keyList);
|
||||
for (int i = keyList.size(); i > 0; i--) { // Inclusive on both ends
|
||||
result.addAll(
|
||||
makeAllFor(i * 3, new SchemaParamSpec("focus", schemas.get(i).getName())));
|
||||
}
|
||||
return matchers(result);
|
||||
}
|
||||
}
|
||||
|
||||
record ReadMemMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
|
||||
static final ReadMemMatcher HAS_PROC_RANGE = new ReadMemMatcher(2, List.of(
|
||||
new TypeParamSpec("process", TargetProcess.class),
|
||||
new TypeParamSpec("range", AddressRange.class)));
|
||||
static final ReadMemMatcher HAS_RANGE = new ReadMemMatcher(1, List.of(
|
||||
new TypeParamSpec("range", AddressRange.class)));
|
||||
static final List<ReadMemMatcher> ALL = matchers(HAS_RANGE);
|
||||
static final List<ReadMemMatcher> ALL = matchers(HAS_PROC_RANGE, HAS_RANGE);
|
||||
}
|
||||
|
||||
record WriteMemMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
|
||||
static final WriteMemMatcher HAS_RANGE = new WriteMemMatcher(1, List.of(
|
||||
static final WriteMemMatcher HAS_PROC_START_DATA = new WriteMemMatcher(2, List.of(
|
||||
new TypeParamSpec("process", TargetProcess.class),
|
||||
new TypeParamSpec("start", Address.class),
|
||||
new TypeParamSpec("data", byte[].class)));
|
||||
static final List<WriteMemMatcher> ALL = matchers(HAS_RANGE);
|
||||
static final WriteMemMatcher HAS_START_DATA = new WriteMemMatcher(1, List.of(
|
||||
new TypeParamSpec("start", Address.class),
|
||||
new TypeParamSpec("data", byte[].class)));
|
||||
static final List<WriteMemMatcher> ALL = matchers(HAS_PROC_START_DATA, HAS_START_DATA);
|
||||
}
|
||||
|
||||
record ReadRegsMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
|
||||
|
@ -469,10 +625,14 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
}
|
||||
|
||||
record WriteRegMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
|
||||
static final WriteRegMatcher HAS_FRAME_NAME_VALUE = new WriteRegMatcher(2, List.of(
|
||||
static final WriteRegMatcher HAS_FRAME_NAME_VALUE = new WriteRegMatcher(3, List.of(
|
||||
new TypeParamSpec("frame", TargetStackFrame.class),
|
||||
new TypeParamSpec("name", String.class),
|
||||
new TypeParamSpec("value", byte[].class)));
|
||||
static final WriteRegMatcher HAS_THREAD_NAME_VALUE = new WriteRegMatcher(2, List.of(
|
||||
new TypeParamSpec("thread", TargetThread.class),
|
||||
new TypeParamSpec("name", String.class),
|
||||
new TypeParamSpec("value", byte[].class)));
|
||||
static final WriteRegMatcher HAS_REG_VALUE = new WriteRegMatcher(1, List.of(
|
||||
new TypeParamSpec("register", TargetRegister.class),
|
||||
new TypeParamSpec("value", byte[].class)));
|
||||
|
@ -480,6 +640,22 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
}
|
||||
|
||||
record BreakExecMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
|
||||
static final BreakExecMatcher HAS_PROC_ADDR_COND_CMDS = new BreakExecMatcher(8, List.of(
|
||||
new TypeParamSpec("process", TargetProcess.class),
|
||||
new TypeParamSpec("address", Address.class),
|
||||
new NameParamSpec("condition", String.class),
|
||||
new NameParamSpec("commands", String.class)));
|
||||
static final BreakExecMatcher HAS_PROC_ADDR_COND = new BreakExecMatcher(7, List.of(
|
||||
new TypeParamSpec("process", TargetProcess.class),
|
||||
new TypeParamSpec("address", Address.class),
|
||||
new NameParamSpec("condition", String.class)));
|
||||
static final BreakExecMatcher HAS_PROC_ADDR_CMDS = new BreakExecMatcher(6, List.of(
|
||||
new TypeParamSpec("process", TargetProcess.class),
|
||||
new TypeParamSpec("address", Address.class),
|
||||
new NameParamSpec("commands", String.class)));
|
||||
static final BreakExecMatcher HAS_PROC_ADDR = new BreakExecMatcher(5, List.of(
|
||||
new TypeParamSpec("process", TargetProcess.class),
|
||||
new TypeParamSpec("address", Address.class)));
|
||||
static final BreakExecMatcher HAS_ADDR_COND_CMDS = new BreakExecMatcher(4, List.of(
|
||||
new TypeParamSpec("address", Address.class),
|
||||
new NameParamSpec("condition", String.class),
|
||||
|
@ -493,10 +669,28 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
static final BreakExecMatcher HAS_ADDR = new BreakExecMatcher(1, List.of(
|
||||
new TypeParamSpec("address", Address.class)));
|
||||
static final List<BreakExecMatcher> ALL =
|
||||
matchers(HAS_ADDR_COND_CMDS, HAS_ADDR_COND, HAS_ADDR_CMDS, HAS_ADDR);
|
||||
matchers(HAS_PROC_ADDR_COND_CMDS, HAS_PROC_ADDR_COND, HAS_PROC_ADDR_CMDS, HAS_PROC_ADDR,
|
||||
HAS_ADDR_COND_CMDS, HAS_ADDR_COND, HAS_ADDR_CMDS, HAS_ADDR);
|
||||
}
|
||||
|
||||
// TODO: Probably need a better way to deal with optional requirements
|
||||
record BreakAccMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
|
||||
static final BreakAccMatcher HAS_PROC_RNG_COND_CMDS = new BreakAccMatcher(8, List.of(
|
||||
new TypeParamSpec("process", TargetProcess.class),
|
||||
new TypeParamSpec("range", AddressRange.class),
|
||||
new NameParamSpec("condition", String.class),
|
||||
new NameParamSpec("commands", String.class)));
|
||||
static final BreakAccMatcher HAS_PROC_RNG_COND = new BreakAccMatcher(7, List.of(
|
||||
new TypeParamSpec("process", TargetProcess.class),
|
||||
new TypeParamSpec("range", AddressRange.class),
|
||||
new NameParamSpec("condition", String.class)));
|
||||
static final BreakAccMatcher HAS_PROC_RNG_CMDS = new BreakAccMatcher(6, List.of(
|
||||
new TypeParamSpec("process", TargetProcess.class),
|
||||
new TypeParamSpec("range", AddressRange.class),
|
||||
new NameParamSpec("commands", String.class)));
|
||||
static final BreakAccMatcher HAS_PROC_RNG = new BreakAccMatcher(5, List.of(
|
||||
new TypeParamSpec("process", TargetProcess.class),
|
||||
new TypeParamSpec("range", AddressRange.class)));
|
||||
static final BreakAccMatcher HAS_RNG_COND_CMDS = new BreakAccMatcher(4, List.of(
|
||||
new TypeParamSpec("range", AddressRange.class),
|
||||
new NameParamSpec("condition", String.class),
|
||||
|
@ -510,7 +704,8 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
static final BreakAccMatcher HAS_RNG = new BreakAccMatcher(1, List.of(
|
||||
new TypeParamSpec("range", AddressRange.class)));
|
||||
static final List<BreakAccMatcher> ALL =
|
||||
matchers(HAS_RNG_COND_CMDS, HAS_RNG_COND, HAS_RNG_CMDS, HAS_RNG);
|
||||
matchers(HAS_PROC_RNG_COND_CMDS, HAS_PROC_RNG_COND, HAS_PROC_RNG_CMDS, HAS_PROC_RNG,
|
||||
HAS_RNG_COND_CMDS, HAS_RNG_COND, HAS_RNG_CMDS, HAS_RNG);
|
||||
}
|
||||
|
||||
record DelBreakMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
|
||||
|
@ -536,6 +731,11 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
protected class Matches {
|
||||
private final Map<String, MatchedMethod> map = new HashMap<>();
|
||||
|
||||
public MatchedMethod getBest(String name, ActionName action,
|
||||
Supplier<List<? extends MethodMatcher>> preferredSupplier) {
|
||||
return map.computeIfAbsent(name, n -> chooseBest(action, preferredSupplier.get()));
|
||||
}
|
||||
|
||||
public MatchedMethod getBest(String name, ActionName action,
|
||||
List<? extends MethodMatcher> preferred) {
|
||||
return map.computeIfAbsent(name, n -> chooseBest(action, preferred));
|
||||
|
@ -554,24 +754,57 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
.max(MatchedMethod::compareTo)
|
||||
.orElse(null);
|
||||
if (best == null) {
|
||||
Msg.error(this, "No suitable " + name + " method");
|
||||
Msg.debug(this, "No suitable " + name + " method");
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
protected static class RequestCaches {
|
||||
final Map<TraceObject, CompletableFuture<Void>> readRegs = new HashMap<>();
|
||||
final Map<Address, CompletableFuture<Void>> readBlock = new HashMap<>();
|
||||
|
||||
public synchronized void invalidate() {
|
||||
readRegs.clear();
|
||||
readBlock.clear();
|
||||
}
|
||||
|
||||
public synchronized CompletableFuture<Void> readRegs(TraceObject obj, RemoteMethod method,
|
||||
Map<String, Object> args) {
|
||||
return readRegs.computeIfAbsent(obj,
|
||||
o -> method.invokeAsync(args).toCompletableFuture().thenApply(__ -> null));
|
||||
}
|
||||
|
||||
public synchronized CompletableFuture<Void> readBlock(Address min, RemoteMethod method,
|
||||
Map<String, Object> args) {
|
||||
return readBlock.computeIfAbsent(min,
|
||||
m -> method.invokeAsync(args).toCompletableFuture().thenApply(__ -> null));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> activateAsync(DebuggerCoordinates prev,
|
||||
DebuggerCoordinates coords) {
|
||||
MatchedMethod activate =
|
||||
matches.getBest("activate", ActionName.ACTIVATE, ActivateMatcher.ALL);
|
||||
if (prev.getSnap() != coords.getSnap()) {
|
||||
requestCaches.invalidate();
|
||||
}
|
||||
TraceObject object = coords.getObject();
|
||||
if (object == null) {
|
||||
return AsyncUtils.nil();
|
||||
}
|
||||
|
||||
SchemaName name = object.getTargetSchema().getName();
|
||||
MatchedMethod activate = matches.getBest("activate_" + name, ActionName.ACTIVATE,
|
||||
() -> ActivateMatcher.makeBySpecificity(trace.getObjectManager().getRootSchema(),
|
||||
object.getCanonicalPath()));
|
||||
if (activate == null) {
|
||||
return AsyncUtils.nil();
|
||||
}
|
||||
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
RemoteParameter paramFocus = activate.params.get("focus");
|
||||
args.put(paramFocus.name(), coords.getObject());
|
||||
args.put(paramFocus.name(),
|
||||
object.querySuitableSchema(getSchemaContext().getSchema(paramFocus.type())));
|
||||
RemoteParameter paramTime = activate.params.get("time");
|
||||
if (paramTime != null) {
|
||||
args.put(paramTime.name(), coords.getTime().toString());
|
||||
|
@ -605,7 +838,29 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
}
|
||||
|
||||
protected SchemaContext getSchemaContext() {
|
||||
return trace.getObjectManager().getRootSchema().getContext();
|
||||
TargetObjectSchema rootSchema = trace.getObjectManager().getRootSchema();
|
||||
if (rootSchema == null) {
|
||||
return null;
|
||||
}
|
||||
return rootSchema.getContext();
|
||||
}
|
||||
|
||||
protected TraceObject getProcessForSpace(AddressSpace space) {
|
||||
for (TraceObjectValue objVal : trace.getObjectManager()
|
||||
.getValuesIntersecting(Lifespan.at(getSnap()),
|
||||
new AddressRangeImpl(space.getMinAddress(), space.getMaxAddress()))) {
|
||||
if (!TargetMemoryRegion.RANGE_ATTRIBUTE_NAME.equals(objVal.getEntryKey())) {
|
||||
continue;
|
||||
}
|
||||
TraceObject obj = objVal.getParent();
|
||||
if (!obj.getInterfaces().contains(TraceObjectMemoryRegion.class)) {
|
||||
continue;
|
||||
}
|
||||
return obj.queryCanonicalAncestorsTargetInterface(TargetProcess.class)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -619,6 +874,15 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
return AsyncUtils.nil();
|
||||
}
|
||||
RemoteParameter paramRange = readMem.params.get("range");
|
||||
RemoteParameter paramProcess = readMem.params.get("process");
|
||||
|
||||
final Map<AddressSpace, TraceObject> procsBySpace;
|
||||
if (paramProcess != null) {
|
||||
procsBySpace = new HashMap<>();
|
||||
}
|
||||
else {
|
||||
procsBySpace = null;
|
||||
}
|
||||
|
||||
int total = 0;
|
||||
AddressSetView quantized = quantize(set);
|
||||
|
@ -630,10 +894,32 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
// NOTE: Don't read in parallel, lest we overload the connection
|
||||
return AsyncUtils.each(TypeSpec.VOID, quantized.iterator(), (r, loop) -> {
|
||||
AddressRangeChunker blocks = new AddressRangeChunker(r, BLOCK_SIZE);
|
||||
if (r.getAddressSpace().isRegisterSpace()) {
|
||||
Msg.warn(this, "Request to read registers via readMemory: " + r + ". Ignoring.");
|
||||
loop.repeatWhile(!monitor.isCancelled());
|
||||
return;
|
||||
}
|
||||
AsyncUtils.each(TypeSpec.VOID, blocks.iterator(), (blk, inner) -> {
|
||||
monitor.incrementProgress(1);
|
||||
RemoteAsyncResult future =
|
||||
readMem.method.invokeAsync(Map.of(paramRange.name(), blk));
|
||||
final Map<String, Object> args;
|
||||
if (paramProcess != null) {
|
||||
TraceObject process = procsBySpace.computeIfAbsent(blk.getAddressSpace(),
|
||||
this::getProcessForSpace);
|
||||
if (process == null) {
|
||||
Msg.warn(this, "Cannot find process containing " + blk.getMinAddress());
|
||||
inner.repeatWhile(!monitor.isCancelled());
|
||||
return;
|
||||
}
|
||||
args = Map.ofEntries(
|
||||
Map.entry(paramProcess.name(), process),
|
||||
Map.entry(paramRange.name(), blk));
|
||||
}
|
||||
else {
|
||||
args = Map.ofEntries(
|
||||
Map.entry(paramRange.name(), blk));
|
||||
}
|
||||
CompletableFuture<Void> future =
|
||||
requestCaches.readBlock(blk.getMinAddress(), readMem.method, args);
|
||||
future.exceptionally(e -> {
|
||||
Msg.error(this, "Could not read " + blk + ": " + e);
|
||||
return null; // Continue looping on errors
|
||||
|
@ -652,6 +938,14 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
Map<String, Object> args = new HashMap<>();
|
||||
args.put(writeMem.params.get("start").name(), address);
|
||||
args.put(writeMem.params.get("data").name(), data);
|
||||
RemoteParameter paramProcess = writeMem.params.get("process");
|
||||
if (paramProcess != null) {
|
||||
TraceObject process = getProcessForSpace(address.getAddressSpace());
|
||||
if (process == null) {
|
||||
throw new IllegalStateException("Cannot find process containing " + address);
|
||||
}
|
||||
args.put(paramProcess.name(), process);
|
||||
}
|
||||
return writeMem.method.invokeAsync(args).toCompletableFuture().thenApply(__ -> null);
|
||||
}
|
||||
|
||||
|
@ -668,11 +962,15 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
return AsyncUtils.nil();
|
||||
}
|
||||
TraceObject container = tot.getObject().queryRegisterContainer(frame);
|
||||
if (container == null) {
|
||||
Msg.error(this,
|
||||
"Cannot find register container for thread,frame: " + thread + "," + frame);
|
||||
return AsyncUtils.nil();
|
||||
}
|
||||
RemoteParameter paramContainer = readRegs.params.get("container");
|
||||
if (paramContainer != null) {
|
||||
return readRegs.method.invokeAsync(Map.of(paramContainer.name(), container))
|
||||
.toCompletableFuture()
|
||||
.thenApply(__ -> null);
|
||||
return requestCaches.readRegs(container, readRegs.method, Map.of(
|
||||
paramContainer.name(), container));
|
||||
}
|
||||
Set<String> keys = new HashSet<>();
|
||||
for (Register r : registers) {
|
||||
|
@ -695,8 +993,8 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
.collect(Collectors.toSet());
|
||||
AsyncFence fence = new AsyncFence();
|
||||
banks.stream().forEach(b -> {
|
||||
fence.include(
|
||||
readRegs.method.invokeAsync(Map.of(paramBank.name(), b)).toCompletableFuture());
|
||||
fence.include(requestCaches.readRegs(b, readRegs.method, Map.of(
|
||||
paramBank.name(), b)));
|
||||
});
|
||||
return fence.ready();
|
||||
}
|
||||
|
@ -704,8 +1002,8 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
if (paramRegister != null) {
|
||||
AsyncFence fence = new AsyncFence();
|
||||
regs.stream().forEach(r -> {
|
||||
fence.include(readRegs.method.invokeAsync(Map.of(paramRegister.name(), r))
|
||||
.toCompletableFuture());
|
||||
fence.include(requestCaches.readRegs(r, readRegs.method, Map.of(
|
||||
paramRegister.name(), r)));
|
||||
});
|
||||
return fence.ready();
|
||||
}
|
||||
|
@ -733,7 +1031,7 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
|
||||
@Override
|
||||
public CompletableFuture<Void> writeRegisterAsync(TracePlatform platform, TraceThread thread,
|
||||
int frame, RegisterValue value) {
|
||||
int frameLevel, RegisterValue value) {
|
||||
MatchedMethod writeReg =
|
||||
matches.getBest("writeReg", ActionName.WRITE_REG, WriteRegMatcher.ALL);
|
||||
if (writeReg == null) {
|
||||
|
@ -748,24 +1046,38 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
byte[] data =
|
||||
Utils.bigIntegerToBytes(value.getUnsignedValue(), register.getMinimumByteSize(), true);
|
||||
|
||||
RemoteParameter paramFrame = writeReg.params.get("frame");
|
||||
if (paramFrame != null) {
|
||||
TraceStack stack = trace.getStackManager().getLatestStack(thread, getSnap());
|
||||
TraceStackFrame frameObj = stack.getFrame(frame, false);
|
||||
RemoteParameter paramThread = writeReg.params.get("thread");
|
||||
if (paramThread != null) {
|
||||
return writeReg.method.invokeAsync(Map.ofEntries(
|
||||
Map.entry(paramFrame.name(), frameObj),
|
||||
Map.entry(paramThread.name(), tot.getObject()),
|
||||
Map.entry(writeReg.params.get("name").name(), regName),
|
||||
Map.entry(writeReg.params.get("data").name(), data)))
|
||||
Map.entry(writeReg.params.get("value").name(), data)))
|
||||
.toCompletableFuture()
|
||||
.thenApply(__ -> null);
|
||||
}
|
||||
TraceObject regObj = findRegisterObject(tot, frame, regName);
|
||||
|
||||
RemoteParameter paramFrame = writeReg.params.get("frame");
|
||||
if (paramFrame != null) {
|
||||
TraceStack stack = trace.getStackManager().getLatestStack(thread, getSnap());
|
||||
TraceStackFrame frame = stack.getFrame(frameLevel, false);
|
||||
if (!(frame instanceof TraceObjectStackFrame tof)) {
|
||||
Msg.error(this, "Non-object trace with TraceRmi!");
|
||||
return AsyncUtils.nil();
|
||||
}
|
||||
return writeReg.method.invokeAsync(Map.ofEntries(
|
||||
Map.entry(paramFrame.name(), tof.getObject()),
|
||||
Map.entry(writeReg.params.get("name").name(), regName),
|
||||
Map.entry(writeReg.params.get("value").name(), data)))
|
||||
.toCompletableFuture()
|
||||
.thenApply(__ -> null);
|
||||
}
|
||||
TraceObject regObj = findRegisterObject(tot, frameLevel, regName);
|
||||
if (regObj == null) {
|
||||
return AsyncUtils.nil();
|
||||
}
|
||||
return writeReg.method.invokeAsync(Map.ofEntries(
|
||||
Map.entry(writeReg.params.get("frame").name(), regObj),
|
||||
Map.entry(writeReg.params.get("data").name(), data)))
|
||||
Map.entry(writeReg.params.get("value").name(), data)))
|
||||
.toCompletableFuture()
|
||||
.thenApply(__ -> null);
|
||||
}
|
||||
|
@ -830,6 +1142,18 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
|
||||
protected void putOptionalBreakArgs(Map<String, Object> args, MatchedMethod brk,
|
||||
String condition, String commands) {
|
||||
RemoteParameter paramProc = brk.params.get("process");
|
||||
if (paramProc != null) {
|
||||
Object proc =
|
||||
findArgumentForSchema(null, getSchemaContext().getSchema(paramProc.type()), true,
|
||||
true, true);
|
||||
if (proc == null) {
|
||||
Msg.error(this, "Cannot find required process argument for " + brk.method);
|
||||
}
|
||||
else {
|
||||
args.put(paramProc.name(), proc);
|
||||
}
|
||||
}
|
||||
if (condition != null && !condition.isBlank()) {
|
||||
RemoteParameter paramCond = brk.params.get("condition");
|
||||
if (paramCond == null) {
|
||||
|
@ -920,21 +1244,21 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
public CompletableFuture<Void> placeBreakpointAsync(AddressRange range,
|
||||
Set<TraceBreakpointKind> kinds, String condition, String commands) {
|
||||
Set<TraceBreakpointKind> copyKinds = Set.copyOf(kinds);
|
||||
if (copyKinds.equals(Set.of(TraceBreakpointKind.HW_EXECUTE))) {
|
||||
if (copyKinds.equals(TraceBreakpointKindSet.HW_EXECUTE)) {
|
||||
return placeHwExecBreakAsync(expectSingleAddr(range, TraceBreakpointKind.HW_EXECUTE),
|
||||
condition, commands);
|
||||
}
|
||||
if (copyKinds.equals(Set.of(TraceBreakpointKind.SW_EXECUTE))) {
|
||||
if (copyKinds.equals(TraceBreakpointKindSet.SW_EXECUTE)) {
|
||||
return placeSwExecBreakAsync(expectSingleAddr(range, TraceBreakpointKind.SW_EXECUTE),
|
||||
condition, commands);
|
||||
}
|
||||
if (copyKinds.equals(Set.of(TraceBreakpointKind.READ))) {
|
||||
if (copyKinds.equals(TraceBreakpointKindSet.READ)) {
|
||||
return placeReadBreakAsync(range, condition, commands);
|
||||
}
|
||||
if (copyKinds.equals(Set.of(TraceBreakpointKind.WRITE))) {
|
||||
if (copyKinds.equals(TraceBreakpointKindSet.WRITE)) {
|
||||
return placeWriteBreakAsync(range, condition, commands);
|
||||
}
|
||||
if (copyKinds.equals(Set.of(TraceBreakpointKind.READ, TraceBreakpointKind.WRITE))) {
|
||||
if (copyKinds.equals(TraceBreakpointKindSet.ACCESS)) {
|
||||
return placeAccessBreakAsync(range, condition, commands);
|
||||
}
|
||||
Msg.error(this, "Invalid kinds in combination: " + kinds);
|
||||
|
@ -986,20 +1310,22 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
return AsyncUtils.nil();
|
||||
}
|
||||
return delBreak.method
|
||||
.invokeAsync(Map.of(delBreak.params.get("specification").name(), spec))
|
||||
.invokeAsync(Map.of(delBreak.params.get("specification").name(), spec.getObject()))
|
||||
.toCompletableFuture()
|
||||
.thenApply(__ -> null);
|
||||
}
|
||||
|
||||
// TODO: Would this make sense for any debugger? To delete individual locations?
|
||||
protected CompletableFuture<Void> deleteBreakpointLocAsync(TraceObjectBreakpointLocation loc) {
|
||||
MatchedMethod delBreak =
|
||||
matches.getBest("delBreakLoc", ActionName.DELETE, DelBreakMatcher.ALL);
|
||||
if (delBreak == null) {
|
||||
return AsyncUtils.nil();
|
||||
Msg.debug(this, "Falling back to delete spec");
|
||||
return deleteBreakpointSpecAsync(loc.getSpecification());
|
||||
}
|
||||
RemoteParameter paramLocation = delBreak.params.get("location");
|
||||
if (paramLocation != null) {
|
||||
return delBreak.method.invokeAsync(Map.of(paramLocation.name(), loc))
|
||||
return delBreak.method.invokeAsync(Map.of(paramLocation.name(), loc.getObject()))
|
||||
.toCompletableFuture()
|
||||
.thenApply(__ -> null);
|
||||
}
|
||||
|
@ -1027,7 +1353,7 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
}
|
||||
return delBreak.method
|
||||
.invokeAsync(Map.ofEntries(
|
||||
Map.entry(delBreak.params.get("specification").name(), spec),
|
||||
Map.entry(delBreak.params.get("specification").name(), spec.getObject()),
|
||||
Map.entry(delBreak.params.get("enabled").name(), enabled)))
|
||||
.toCompletableFuture()
|
||||
.thenApply(__ -> null);
|
||||
|
@ -1038,18 +1364,19 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
MatchedMethod delBreak =
|
||||
matches.getBest("toggleBreakLoc", ActionName.TOGGLE, ToggleBreakMatcher.ALL);
|
||||
if (delBreak == null) {
|
||||
return AsyncUtils.nil();
|
||||
Msg.debug(this, "Falling back to toggle spec");
|
||||
return toggleBreakpointSpecAsync(loc.getSpecification(), enabled);
|
||||
}
|
||||
RemoteParameter paramLocation = delBreak.params.get("location");
|
||||
if (paramLocation != null) {
|
||||
return delBreak.method
|
||||
.invokeAsync(Map.ofEntries(
|
||||
Map.entry(paramLocation.name(), loc),
|
||||
Map.entry(paramLocation.name(), loc.getObject()),
|
||||
Map.entry(delBreak.params.get("enabled").name(), enabled)))
|
||||
.toCompletableFuture()
|
||||
.thenApply(__ -> null);
|
||||
}
|
||||
return deleteBreakpointSpecAsync(loc.getSpecification());
|
||||
return toggleBreakpointSpecAsync(loc.getSpecification(), enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1065,6 +1392,23 @@ public class TraceRmiTarget extends AbstractTarget {
|
|||
return AsyncUtils.nil();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> forceTerminateAsync() {
|
||||
Map<String, ActionEntry> kills = collectKillActions(null);
|
||||
for (ActionEntry kill : kills.values()) {
|
||||
if (kill.requiresPrompt()) {
|
||||
continue;
|
||||
}
|
||||
return kill.invokeAsync(false).handle((v, e) -> {
|
||||
connection.forceCloseTrace(trace);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
Msg.warn(this, "Cannot find way to gracefully kill. Forcing close regardless.");
|
||||
connection.forceCloseTrace(trace);
|
||||
return AsyncUtils.nil();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> disconnectAsync() {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,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;
|
||||
|
@ -32,7 +31,7 @@ import ghidra.trace.model.memory.*;
|
|||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
public class VisibleROOnceAutoReadMemorySpec implements AutoReadMemorySpec {
|
||||
public static final String CONFIG_NAME = "READ_VIS_RO_ONCE";
|
||||
public static final String CONFIG_NAME = "1_READ_VIS_RO_ONCE";
|
||||
|
||||
@Override
|
||||
public String getConfigName() {
|
||||
|
@ -50,10 +49,10 @@ public class VisibleROOnceAutoReadMemorySpec 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();
|
||||
|
@ -62,7 +61,7 @@ public class VisibleROOnceAutoReadMemorySpec implements AutoReadMemorySpec {
|
|||
AddressSet toRead = visible.subtract(alreadyKnown);
|
||||
|
||||
if (toRead.isEmpty()) {
|
||||
return AsyncUtils.nil();
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
AddressSet everKnown = new AddressSet();
|
||||
|
@ -85,9 +84,9 @@ public class VisibleROOnceAutoReadMemorySpec implements AutoReadMemorySpec {
|
|||
toRead.delete(everKnown.intersect(readOnly));
|
||||
|
||||
if (toRead.isEmpty()) {
|
||||
return AsyncUtils.nil();
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
return target.readMemoryAsync(toRead, TaskMonitor.DUMMY);
|
||||
return target.readMemoryAsync(toRead, TaskMonitor.DUMMY).thenApply(__ -> true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -165,6 +165,11 @@ public class WatchLocationTrackingSpec implements LocationTrackingSpec {
|
|||
public boolean affectedByStackChange(TraceStack stack, DebuggerCoordinates coordinates) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDisassemble() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -620,7 +620,8 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter
|
|||
|
||||
private boolean isVisible(TraceBreakpoint location) {
|
||||
long snap = traceManager.getCurrentFor(trace).getSnap();
|
||||
return location.getLifespan().contains(snap);
|
||||
Lifespan span = location.getLifespan();
|
||||
return span != null && span.contains(snap);
|
||||
}
|
||||
|
||||
private void locationAdded(TraceBreakpoint location) {
|
||||
|
|
|
@ -86,15 +86,15 @@ public class DebuggerConsolePlugin extends Plugin implements DebuggerConsoleServ
|
|||
provider = new DebuggerConsoleProvider(this);
|
||||
|
||||
rootLogger = (Logger) LogManager.getRootLogger();
|
||||
appender.start();
|
||||
rootLogger.addAppender(appender);
|
||||
//appender.start();
|
||||
//rootLogger.addAppender(appender);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispose() {
|
||||
if (rootLogger != null) {
|
||||
rootLogger.removeAppender(appender);
|
||||
appender.stop();
|
||||
//rootLogger.removeAppender(appender);
|
||||
//appender.stop();
|
||||
|
||||
provider.dispose();
|
||||
tool.removeComponentProvider(provider);
|
||||
|
|
|
@ -13,13 +13,14 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.services;
|
||||
package ghidra.app.plugin.core.debug.gui.control;
|
||||
|
||||
import ghidra.framework.plugintool.ServiceInfo;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
|
||||
@ServiceInfo(
|
||||
defaultProviderName = "ghidra.app.plugin.core.debug.service.workflow.DebuggerWorkflowServiceProxyPlugin",
|
||||
description = "Service for managing automatic debugger actions and analysis")
|
||||
public interface DebuggerWorkflowToolService extends DebuggerWorkflowService {
|
||||
interface ControlAction {
|
||||
String GROUP = DebuggerResources.GROUP_CONTROL;
|
||||
|
||||
static String intSubGroup(int subGroup) {
|
||||
return String.format("%02d", subGroup);
|
||||
}
|
||||
}
|
|
@ -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.control;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.action.ToolBarData;
|
||||
import docking.menu.ActionState;
|
||||
import docking.menu.MultiStateDockingAction;
|
||||
import docking.widgets.EventTrigger;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.debug.api.control.ControlMode;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
class ControlModeAction extends MultiStateDockingAction<ControlMode> {
|
||||
public static final String NAME = "Control Mode";
|
||||
public static final String DESCRIPTION = "Choose what to control and edit in dynamic views";
|
||||
public static final String GROUP = DebuggerResources.GROUP_CONTROL;
|
||||
public static final String HELP_ANCHOR = "control_mode";
|
||||
|
||||
private final DebuggerControlPlugin plugin;
|
||||
|
||||
public ControlModeAction(DebuggerControlPlugin plugin) {
|
||||
super(NAME, plugin.getName());
|
||||
this.plugin = plugin;
|
||||
setDescription(DESCRIPTION);
|
||||
setToolBarData(new ToolBarData(DebuggerResources.ICON_BLANK, GROUP, ""));
|
||||
setHelpLocation(new HelpLocation(getOwner(), HELP_ANCHOR));
|
||||
setActionStates(ControlMode.ALL.stream()
|
||||
.map(m -> new ActionState<>(m.name, m.icon, m))
|
||||
.collect(Collectors.toList()));
|
||||
setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabledForContext(ActionContext context) {
|
||||
return plugin.current.getTrace() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isStateEnabled(ActionState<ControlMode> state) {
|
||||
return state.getUserData().isSelectable(plugin.current);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionStateChanged(ActionState<ControlMode> newActionState,
|
||||
EventTrigger trigger) {
|
||||
plugin.activateControlMode(newActionState, trigger);
|
||||
}
|
||||
}
|
|
@ -15,28 +15,22 @@
|
|||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.control;
|
||||
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.*;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.DockingContextListener;
|
||||
import docking.action.*;
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import docking.action.DockingAction;
|
||||
import docking.action.DockingActionIf;
|
||||
import docking.menu.ActionState;
|
||||
import docking.menu.MultiStateDockingAction;
|
||||
import docking.widgets.EventTrigger;
|
||||
import ghidra.app.context.ProgramLocationActionContext;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin;
|
||||
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.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.app.plugin.core.debug.service.model.TraceRecorderTarget;
|
||||
import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.app.services.DebuggerControlService.ControlModeChangeListener;
|
||||
import ghidra.app.services.DebuggerEmulationService.CachedEmulator;
|
||||
|
@ -52,12 +46,15 @@ import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
|||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.trace.model.*;
|
||||
import ghidra.trace.model.Trace.TraceObjectChangeType;
|
||||
import ghidra.trace.model.target.TraceObjectValue;
|
||||
import ghidra.trace.model.time.schedule.Scheduler;
|
||||
import ghidra.trace.model.time.schedule.TraceSchedule;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.Swing;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.Task;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
@ -79,316 +76,6 @@ import ghidra.util.task.TaskMonitor;
|
|||
public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
||||
implements DockingContextListener {
|
||||
|
||||
static String intSubGroup(int subGroup) {
|
||||
return String.format("%02d", subGroup);
|
||||
}
|
||||
|
||||
interface ControlAction {
|
||||
String GROUP = DebuggerResources.GROUP_CONTROL;
|
||||
}
|
||||
|
||||
protected class ControlModeAction extends MultiStateDockingAction<ControlMode> {
|
||||
public static final String NAME = "Control Mode";
|
||||
public static final String DESCRIPTION = "Choose what to control and edit in dynamic views";
|
||||
public static final String GROUP = DebuggerResources.GROUP_CONTROL;
|
||||
public static final String HELP_ANCHOR = "control_mode";
|
||||
|
||||
public ControlModeAction() {
|
||||
super(NAME, DebuggerControlPlugin.this.getName());
|
||||
setDescription(DESCRIPTION);
|
||||
setToolBarData(new ToolBarData(DebuggerResources.ICON_BLANK, GROUP, ""));
|
||||
setHelpLocation(new HelpLocation(getOwner(), HELP_ANCHOR));
|
||||
setActionStates(ControlMode.ALL.stream()
|
||||
.map(m -> new ActionState<>(m.name, m.icon, m))
|
||||
.collect(Collectors.toList()));
|
||||
setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabledForContext(ActionContext context) {
|
||||
return current.getTrace() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isStateEnabled(ActionState<ControlMode> state) {
|
||||
return state.getUserData().isSelectable(current);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionStateChanged(ActionState<ControlMode> newActionState,
|
||||
EventTrigger trigger) {
|
||||
activateControlMode(newActionState, trigger);
|
||||
}
|
||||
}
|
||||
|
||||
interface ResumeAction extends ControlAction {
|
||||
Icon ICON = DebuggerResources.ICON_RESUME;
|
||||
int SUB_GROUP = 0;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0);
|
||||
}
|
||||
|
||||
interface TargetResumeAction extends ResumeAction {
|
||||
String HELP_ANCHOR = "target_resume";
|
||||
|
||||
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(name, ownerName)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface EmulateResumeAction extends ResumeAction {
|
||||
String NAME = "Resume Emulator";
|
||||
String DESCRIPTION = "Resume, i.e., go or continue execution of the integrated emulator";
|
||||
String HELP_ANCHOR = "emu_resume";
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface InterruptAction extends ControlAction {
|
||||
Icon ICON = DebuggerResources.ICON_INTERRUPT;
|
||||
int SUB_GROUP = 1;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_I, KeyEvent.CTRL_DOWN_MASK);
|
||||
}
|
||||
|
||||
interface TargetInterruptAction extends InterruptAction {
|
||||
String HELP_ANCHOR = "target_interrupt";
|
||||
|
||||
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(name, ownerName)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface EmulateInterruptAction extends InterruptAction {
|
||||
String NAME = "Interrupt Emulator";
|
||||
String DESCRIPTION = "Interrupt, i.e., suspend, the integrated emulator";
|
||||
String HELP_ANCHOR = "emu_interrupt";
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface TargetKillAction extends ControlAction {
|
||||
Icon ICON = DebuggerResources.ICON_KILL;
|
||||
String HELP_ANCHOR = "target_kill";
|
||||
int SUB_GROUP = 2;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_K,
|
||||
KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK);
|
||||
|
||||
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(name, ownerName)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface DisconnectAction extends ControlAction {
|
||||
String NAME = "Disconnect";
|
||||
String DESCRIPTION = "Close the connection to the debugging agent";
|
||||
Icon ICON = DebuggerResources.ICON_DISCONNECT;
|
||||
String HELP_ANCHOR = "target_disconnect";
|
||||
int SUB_GROUP = 3;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_K,
|
||||
KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK);
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface EmulateStepBackAction extends ControlAction {
|
||||
String NAME = "Step Emulator Back";
|
||||
String DESCRIPTION = "Step the integrated emulator a single instruction backward";
|
||||
Icon ICON = DebuggerResources.ICON_STEP_BACK;
|
||||
String HELP_ANCHOR = "emu_step_back";
|
||||
int SUB_GROUP = 4;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0);
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface StepIntoAction extends ControlAction {
|
||||
Icon ICON = DebuggerResources.ICON_STEP_INTO;
|
||||
int SUB_GROUP = 5;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0);
|
||||
}
|
||||
|
||||
interface TargetStepIntoAction extends StepIntoAction {
|
||||
String HELP_ANCHOR = "target_step_into";
|
||||
|
||||
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(name, ownerName)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface EmulateStepIntoAction extends StepIntoAction {
|
||||
String NAME = "Step Emulator Into";
|
||||
String DESCRIPTION =
|
||||
"Step the integrated emulator a single instruction, descending into calls";
|
||||
String HELP_ANCHOR = "emu_step_into";
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface TargetStepOverAction extends ControlAction {
|
||||
Icon ICON = DebuggerResources.ICON_STEP_OVER;
|
||||
String HELP_ANCHOR = "target_step_over";
|
||||
int SUB_GROUP = 6;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F10, 0);
|
||||
|
||||
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(name, ownerName)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface EmulateSkipOverAction extends ControlAction {
|
||||
String NAME = "Skip Emulator";
|
||||
String DESCRIPTION =
|
||||
"Skip the integrated emulator a single instruction, ignoring its effects";
|
||||
Icon ICON = DebuggerResources.ICON_SKIP_OVER;
|
||||
String HELP_ANCHOR = "emu_skip_over";
|
||||
int SUB_GROUP = 7;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F10, KeyEvent.CTRL_DOWN_MASK);
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface TargetStepOutAction extends ControlAction {
|
||||
Icon ICON = DebuggerResources.ICON_STEP_FINISH;
|
||||
String HELP_ANCHOR = "target_step_out";
|
||||
int SUB_GROUP = 8;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F12, 0);
|
||||
|
||||
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(name, ownerName)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface TargetStepExtAction extends ControlAction {
|
||||
Icon ICON = DebuggerResources.ICON_STEP_LAST;
|
||||
String HELP_ANCHOR = "target_step_ext";
|
||||
int SUB_GROUP = 9;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, KeyEvent.CTRL_DOWN_MASK);
|
||||
|
||||
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(name, ownerName)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP) + name)
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface TraceSnapBackwardAction extends ControlAction {
|
||||
String NAME = "Trace Snapshot Backward";
|
||||
String DESCRIPTION = "Navigate the trace recording backward one snapshot";
|
||||
Icon ICON = DebuggerResources.ICON_SNAP_BACKWARD;
|
||||
String HELP_ANCHOR = "trace_snap_backward";
|
||||
int SUB_GROUP = 10;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0);
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface TraceSnapForwardAction extends ControlAction {
|
||||
String NAME = "Trace Snapshot Forward";
|
||||
String DESCRIPTION = "Navigate the trace recording forward one snapshot";
|
||||
Icon ICON = DebuggerResources.ICON_SNAP_FORWARD;
|
||||
String HELP_ANCHOR = "trace_snap_backward";
|
||||
int SUB_GROUP = 11;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0);
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
private final TraceDomainObjectListener listenerForObjects = new TraceDomainObjectListener() {
|
||||
{
|
||||
listenFor(TraceObjectChangeType.VALUE_CREATED, this::valueChanged);
|
||||
|
@ -427,8 +114,16 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
|||
|
||||
protected MultiStateDockingAction<ControlMode> actionControlMode;
|
||||
|
||||
DockingAction actionTargetResume;
|
||||
DockingAction actionTargetInterrupt;
|
||||
DockingAction actionTargetKill;
|
||||
DockingAction actionTargetStepInto;
|
||||
DockingAction actionTargetStepOver;
|
||||
DockingAction actionTargetStepOut;
|
||||
DockingAction actionTargetDisconnect;
|
||||
final Set<DockingAction> actionsTarget = new HashSet<>();
|
||||
Set<DockingAction> actionsTarget;
|
||||
final Set<DockingAction> actionsTargetStepExt = new HashSet<>();
|
||||
final Set<DockingAction> actionsTargetAll = new HashSet<>();
|
||||
|
||||
DockingAction actionEmulateResume;
|
||||
DockingAction actionEmulateInterrupt;
|
||||
|
@ -442,7 +137,6 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
|||
Set<DockingAction> actionsTrace;
|
||||
|
||||
Set<Set<DockingAction>> actionSets;
|
||||
Collection<? extends DockingActionIf> curActionSet;
|
||||
|
||||
ActionContext context;
|
||||
|
||||
|
@ -463,7 +157,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
|||
switch (mode) {
|
||||
case RO_TARGET:
|
||||
case RW_TARGET:
|
||||
return actionsTarget;
|
||||
return actionsTargetAll;
|
||||
case RO_TRACE:
|
||||
case RW_TRACE:
|
||||
return actionsTrace;
|
||||
|
@ -475,7 +169,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
|||
}
|
||||
|
||||
protected Set<DockingAction> getActionSet() {
|
||||
return getActionSet(computeCurrentEditingMode());
|
||||
return getActionSet(computeCurrentControlMode());
|
||||
}
|
||||
|
||||
protected void updateActionsEnabled(ControlMode mode) {
|
||||
|
@ -485,24 +179,70 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
|||
}
|
||||
|
||||
protected void updateActionsEnabled() {
|
||||
updateActionsEnabled(computeCurrentEditingMode());
|
||||
updateActionsEnabled(computeCurrentControlMode());
|
||||
}
|
||||
|
||||
protected static boolean isSameContext(ActionContext ctx1, ActionContext ctx2) {
|
||||
if (ctx1 instanceof ProgramLocationActionContext locCtx1) {
|
||||
if (!(ctx2 instanceof ProgramLocationActionContext locCtx2)) {
|
||||
return false;
|
||||
}
|
||||
Program prog1 = locCtx1.getProgram();
|
||||
Program prog2 = locCtx2.getProgram();
|
||||
if (prog1 != prog2) {
|
||||
return false;
|
||||
}
|
||||
Address addr1 = locCtx1.getAddress();
|
||||
Address addr2 = locCtx2.getAddress();
|
||||
if (!Objects.equals(addr1, addr2)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (ctx1 instanceof DebuggerObjectActionContext objCtx1) {
|
||||
if (!(ctx2 instanceof DebuggerObjectActionContext objCtx2)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(objCtx1.getObjectValues(), objCtx2.getObjectValues());
|
||||
}
|
||||
return true; // Treat all unknowns as same.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contextChanged(ActionContext context) {
|
||||
boolean same = isSameContext(this.context, context);
|
||||
this.context = context;
|
||||
updateActionsEnabled();
|
||||
if (same) {
|
||||
return;
|
||||
}
|
||||
updateTargetStepExtActions();
|
||||
updateActions();
|
||||
}
|
||||
|
||||
protected void createActions() {
|
||||
actionControlMode = new ControlModeAction();
|
||||
actionControlMode = new ControlModeAction(this);
|
||||
tool.addAction(actionControlMode);
|
||||
|
||||
actionTargetResume = TargetResumeAction.builder(this)
|
||||
.build();
|
||||
actionTargetInterrupt = TargetInterruptAction.builder(this)
|
||||
.build();
|
||||
actionTargetKill = TargetKillAction.builder(this)
|
||||
.build();
|
||||
actionTargetStepInto = TargetStepIntoAction.builder(this)
|
||||
.build();
|
||||
actionTargetStepOver = TargetStepOverAction.builder(this)
|
||||
.build();
|
||||
actionTargetStepOut = TargetStepOutAction.builder(this)
|
||||
.build();
|
||||
actionTargetDisconnect = DisconnectAction.builder(this)
|
||||
.enabledWhen(this::isActionTargetDisconnectEnabled)
|
||||
.onAction(this::activatedTargetDisconnect)
|
||||
.build();
|
||||
updateTargetActions();
|
||||
actionsTarget = Set.of(actionTargetResume, actionTargetInterrupt, actionTargetKill,
|
||||
actionTargetStepInto, actionTargetStepOver, actionTargetStepOut,
|
||||
actionTargetDisconnect);
|
||||
updateTargetStepExtActions();
|
||||
|
||||
actionEmulateResume = EmulateResumeAction.builder(this)
|
||||
.enabledWhen(this::isActionEmulateResumeEnabled)
|
||||
|
@ -537,51 +277,37 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
|||
.build();
|
||||
actionsTrace = Set.of(actionTraceSnapBackward, actionTraceSnapForward);
|
||||
|
||||
actionSets = Set.of(actionsTarget, actionsEmulate, actionsTrace);
|
||||
actionSets = Set.of(actionsTargetAll, actionsEmulate, actionsTrace);
|
||||
|
||||
updateActions();
|
||||
}
|
||||
|
||||
protected interface TargetActionBuilderFactory
|
||||
extends BiFunction<String, DebuggerControlPlugin, ActionBuilder> {
|
||||
protected void runTask(String title, ActionEntry entry) {
|
||||
tool.execute(new TargetActionTask(title, entry));
|
||||
}
|
||||
|
||||
protected DockingAction buildTargetAction(TargetActionBuilderFactory factory,
|
||||
ActionEntry entry) {
|
||||
return factory.apply(entry.display(), this)
|
||||
.description(entry.details())
|
||||
.enabledWhen(ctx -> entry.isEnabled())
|
||||
.onAction(ctx -> runTask(entry))
|
||||
.build();
|
||||
}
|
||||
|
||||
protected void runTask(ActionEntry entry) {
|
||||
tool.execute(new TargetActionTask(entry));
|
||||
}
|
||||
|
||||
protected void addTargetActionsForName(Target target, ActionName name,
|
||||
TargetActionBuilderFactory factory) {
|
||||
for (ActionEntry entry : target.collectActions(name, context).values()) {
|
||||
protected void addTargetStepExtActions(Target target) {
|
||||
for (ActionEntry entry : target.collectActions(ActionName.STEP_EXT, context).values()) {
|
||||
if (entry.requiresPrompt()) {
|
||||
continue;
|
||||
}
|
||||
actionsTarget.add(buildTargetAction(factory, entry));
|
||||
actionsTargetStepExt.add(TargetStepExtAction.builder(entry.display(), this)
|
||||
.description(entry.details())
|
||||
.enabledWhen(ctx -> entry.isEnabled())
|
||||
.onAction(ctx -> runTask(entry.display(), entry))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is for testing purposes only. Fetch an action from "targetActions" whose name matches
|
||||
* that given.
|
||||
*
|
||||
* <p>
|
||||
* Since the tests are still assuming {@link TraceRecorderTarget}s, actions can be reliably
|
||||
* retrieved by name.
|
||||
* This is for testing purposes only. Fetch an action from {@link #actionsTargetStepExt} whose
|
||||
* name matches that given.
|
||||
*
|
||||
* @param name the action name
|
||||
* @return the action, or null
|
||||
*/
|
||||
/* testing */ DockingAction getTargetAction(String name) {
|
||||
for (DockingAction action : actionsTarget) {
|
||||
/* testing */ DockingAction getTargetStepExtAction(String name) {
|
||||
for (DockingAction action : actionsTargetStepExt) {
|
||||
if (name.equals(action.getName())) {
|
||||
return action;
|
||||
}
|
||||
|
@ -589,23 +315,19 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
|||
return null;
|
||||
}
|
||||
|
||||
protected void updateTargetActions() {
|
||||
hideActions(actionsTarget);
|
||||
actionsTarget.clear();
|
||||
actionsTarget.add(actionTargetDisconnect);
|
||||
protected void updateTargetStepExtActions() {
|
||||
hideActions(actionsTargetStepExt);
|
||||
actionsTargetStepExt.clear();
|
||||
actionsTargetAll.clear();
|
||||
actionsTargetAll.addAll(actionsTarget);
|
||||
|
||||
Target target = current.getTarget();
|
||||
if (target == null || !target.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
addTargetActionsForName(target, ActionName.RESUME, TargetResumeAction::builder);
|
||||
addTargetActionsForName(target, ActionName.INTERRUPT, TargetInterruptAction::builder);
|
||||
addTargetActionsForName(target, ActionName.KILL, TargetKillAction::builder);
|
||||
addTargetActionsForName(target, ActionName.STEP_INTO, TargetStepIntoAction::builder);
|
||||
addTargetActionsForName(target, ActionName.STEP_OVER, TargetStepOverAction::builder);
|
||||
addTargetActionsForName(target, ActionName.STEP_OUT, TargetStepOutAction::builder);
|
||||
addTargetActionsForName(target, ActionName.STEP_EXT, TargetStepExtAction::builder);
|
||||
addTargetStepExtActions(target);
|
||||
actionsTargetAll.addAll(actionsTargetStepExt);
|
||||
}
|
||||
|
||||
protected void activateControlMode(ActionState<ControlMode> state, EventTrigger trigger) {
|
||||
|
@ -792,7 +514,9 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
|||
}
|
||||
|
||||
protected void coordinatesActivated(DebuggerCoordinates coords) {
|
||||
boolean sameTrace = true;
|
||||
if (current.getTrace() != coords.getTrace()) {
|
||||
sameTrace = false;
|
||||
if (current.getTrace() != null) {
|
||||
current.getTrace().removeListener(listenerForObjects);
|
||||
}
|
||||
|
@ -801,11 +525,13 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
|||
}
|
||||
}
|
||||
current = coords;
|
||||
updateTargetActions();
|
||||
if (!sameTrace) {
|
||||
updateTargetStepExtActions();
|
||||
}
|
||||
updateActions();
|
||||
}
|
||||
|
||||
private ControlMode computeCurrentEditingMode() {
|
||||
private ControlMode computeCurrentControlMode() {
|
||||
if (controlService == null) {
|
||||
return ControlMode.DEFAULT;
|
||||
}
|
||||
|
@ -819,9 +545,6 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
|||
if (tool == null) {
|
||||
return;
|
||||
}
|
||||
if (curActionSet == actions) {
|
||||
curActionSet = null;
|
||||
}
|
||||
for (DockingActionIf action : actions) {
|
||||
tool.removeAction(action);
|
||||
}
|
||||
|
@ -831,17 +554,16 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
|||
if (tool == null) {
|
||||
return;
|
||||
}
|
||||
if (curActionSet == actions) {
|
||||
return;
|
||||
}
|
||||
Set<DockingActionIf> already = tool.getDockingActionsByOwnerName(name);
|
||||
for (DockingActionIf action : actions) {
|
||||
tool.addAction(action);
|
||||
if (!already.contains(action)) {
|
||||
tool.addAction(action);
|
||||
}
|
||||
}
|
||||
curActionSet = actions;
|
||||
}
|
||||
|
||||
private void updateActions() {
|
||||
ControlMode mode = computeCurrentEditingMode();
|
||||
ControlMode mode = computeCurrentControlMode();
|
||||
actionControlMode.setCurrentActionStateByUserData(mode);
|
||||
|
||||
Set<DockingAction> actions = getActionSet(mode);
|
||||
|
@ -860,6 +582,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
|||
if (current.getTrace() == trace) {
|
||||
trace.removeListener(listenerForObjects);
|
||||
current = DebuggerCoordinates.NOWHERE;
|
||||
updateTargetStepExtActions();
|
||||
}
|
||||
updateActions();
|
||||
}
|
||||
|
|
|
@ -23,8 +23,10 @@ import docking.Tool;
|
|||
import docking.action.*;
|
||||
import docking.actions.PopupActionProvider;
|
||||
import ghidra.app.context.ProgramActionContext;
|
||||
import ghidra.app.context.ProgramLocationActionContext;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
|
||||
import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.debug.api.control.ControlMode;
|
||||
import ghidra.debug.api.target.Target;
|
||||
|
@ -62,7 +64,7 @@ public class DebuggerMethodActionsPlugin extends Plugin implements PopupActionPr
|
|||
|
||||
@Override
|
||||
public void actionPerformed(ActionContext context) {
|
||||
tool.execute(new TargetActionTask(entry));
|
||||
tool.execute(new TargetActionTask(entry.display(), entry));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,11 +97,28 @@ public class DebuggerMethodActionsPlugin extends Plugin implements PopupActionPr
|
|||
return mode.isTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* While it may be possible to retrieve sufficient context from the "current state," it's not
|
||||
* always appropriate to display all of these actions. They should really only appear when an
|
||||
* address is clearly intended, or when a trace object is clearly intended. We'll have to see
|
||||
* how/if this works in the type-specific trace object tables, e.g., the Modules panel.
|
||||
*/
|
||||
protected boolean isAppropriate(ActionContext context) {
|
||||
if (context instanceof ProgramLocationActionContext) {
|
||||
return true;
|
||||
}
|
||||
if (context instanceof DebuggerObjectActionContext) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DockingActionIf> getPopupActions(Tool tool, ActionContext context) {
|
||||
if (!isControlTarget()) {
|
||||
if (!isControlTarget() || !isAppropriate(context)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
Target target = getTarget(context);
|
||||
if (target == null) {
|
||||
return List.of();
|
||||
|
@ -107,6 +126,9 @@ public class DebuggerMethodActionsPlugin extends Plugin implements PopupActionPr
|
|||
|
||||
List<DockingActionIf> result = new ArrayList<>();
|
||||
for (ActionEntry entry : target.collectActions(null, context).values()) {
|
||||
if (entry.requiresPrompt() || entry.builtIn()) {
|
||||
continue;
|
||||
}
|
||||
result.add(new InvokeActionEntryAction(entry));
|
||||
}
|
||||
return result;
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface DisconnectAction extends ControlAction {
|
||||
String NAME = "Disconnect";
|
||||
String DESCRIPTION = "Close the connection to the debugging agent";
|
||||
Icon ICON = DebuggerResources.ICON_DISCONNECT;
|
||||
String HELP_ANCHOR = "target_disconnect";
|
||||
int SUB_GROUP = 3;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_K,
|
||||
KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK);
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface EmulateInterruptAction extends InterruptAction {
|
||||
String NAME = "Interrupt Emulator";
|
||||
String DESCRIPTION = "Interrupt, i.e., suspend, the integrated emulator";
|
||||
String HELP_ANCHOR = "emu_interrupt";
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface EmulateResumeAction extends ResumeAction {
|
||||
String NAME = "Resume Emulator";
|
||||
String DESCRIPTION = "Resume, i.e., go or continue execution of the integrated emulator";
|
||||
String HELP_ANCHOR = "emu_resume";
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface EmulateSkipOverAction extends ControlAction {
|
||||
String NAME = "Skip Emulator";
|
||||
String DESCRIPTION =
|
||||
"Skip the integrated emulator a single instruction, ignoring its effects";
|
||||
Icon ICON = DebuggerResources.ICON_SKIP_OVER;
|
||||
String HELP_ANCHOR = "emu_skip_over";
|
||||
int SUB_GROUP = 7;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F10, KeyEvent.CTRL_DOWN_MASK);
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.control;
|
||||
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface EmulateStepBackAction extends ControlAction {
|
||||
String NAME = "Step Emulator Back";
|
||||
String DESCRIPTION = "Step the integrated emulator a single instruction backward";
|
||||
Icon ICON = DebuggerResources.ICON_STEP_BACK;
|
||||
String HELP_ANCHOR = "emu_step_back";
|
||||
int SUB_GROUP = 4;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0);
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface EmulateStepIntoAction extends StepIntoAction {
|
||||
String NAME = "Step Emulator Into";
|
||||
String DESCRIPTION =
|
||||
"Step the integrated emulator a single instruction, descending into calls";
|
||||
String HELP_ANCHOR = "emu_step_into";
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -13,23 +13,17 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.services;
|
||||
package ghidra.app.plugin.core.debug.gui.control;
|
||||
|
||||
import java.util.Set;
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import ghidra.debug.api.workflow.DebuggerBot;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
public interface DebuggerWorkflowService {
|
||||
PluginTool getTool();
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
|
||||
Set<DebuggerBot> getAllBots();
|
||||
|
||||
Set<DebuggerBot> getEnabledBots();
|
||||
|
||||
Set<DebuggerBot> getDisabledBots();
|
||||
|
||||
void enableBots(Set<DebuggerBot> actors);
|
||||
|
||||
void disableBots(Set<DebuggerBot> actors);
|
||||
interface InterruptAction extends ControlAction {
|
||||
Icon ICON = DebuggerResources.ICON_INTERRUPT;
|
||||
int SUB_GROUP = 1;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_I, KeyEvent.CTRL_DOWN_MASK);
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
|
||||
interface ResumeAction extends ControlAction {
|
||||
Icon ICON = DebuggerResources.ICON_RESUME;
|
||||
int SUB_GROUP = 0;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0);
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
|
||||
interface StepIntoAction extends ControlAction {
|
||||
Icon ICON = DebuggerResources.ICON_STEP_INTO;
|
||||
int SUB_GROUP = 5;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0);
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.action.builder.AbstractActionBuilder;
|
||||
import ghidra.debug.api.target.ActionName;
|
||||
|
||||
class TargetActionBuilder
|
||||
extends AbstractActionBuilder<TargetDockingAction, ActionContext, TargetActionBuilder> {
|
||||
private final DebuggerControlPlugin plugin;
|
||||
|
||||
private ActionName action;
|
||||
private String defaultDescription;
|
||||
|
||||
public TargetActionBuilder(String name, DebuggerControlPlugin plugin) {
|
||||
super(name, plugin.getName());
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TargetActionBuilder self() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void validate() {
|
||||
super.validate();
|
||||
if (action == null) {
|
||||
throw new IllegalStateException(
|
||||
"Can't build a " + TargetDockingAction.class.getSimpleName() +
|
||||
" without an action name");
|
||||
}
|
||||
}
|
||||
|
||||
public TargetActionBuilder action(ActionName action) {
|
||||
this.action = action;
|
||||
return self();
|
||||
}
|
||||
|
||||
public TargetActionBuilder defaultDescription(String defaultDescription) {
|
||||
this.defaultDescription = defaultDescription;
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetDockingAction build() {
|
||||
onAction(ctx -> {
|
||||
// Make the super.validate() hush
|
||||
});
|
||||
validate();
|
||||
TargetDockingAction result = new TargetDockingAction(name, owner, keyBindingType, plugin,
|
||||
action, defaultDescription);
|
||||
decorateAction(result);
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -23,8 +23,8 @@ import ghidra.util.task.TaskMonitor;
|
|||
class TargetActionTask extends Task {
|
||||
private ActionEntry entry;
|
||||
|
||||
public TargetActionTask(ActionEntry entry) {
|
||||
super(entry.display(), false, false, false);
|
||||
public TargetActionTask(String title, ActionEntry entry) {
|
||||
super(title, false, false, false);
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.action.DockingAction;
|
||||
import docking.action.KeyBindingType;
|
||||
import ghidra.debug.api.target.ActionName;
|
||||
import ghidra.debug.api.target.Target;
|
||||
import ghidra.debug.api.target.Target.ActionEntry;
|
||||
|
||||
class TargetDockingAction extends DockingAction {
|
||||
private final DebuggerControlPlugin plugin;
|
||||
private final ActionName action;
|
||||
private final String defaultDescription;
|
||||
|
||||
private ActionEntry entry;
|
||||
|
||||
public TargetDockingAction(String name, String owner, KeyBindingType keyBindingType,
|
||||
DebuggerControlPlugin plugin, ActionName action, String defaultDescription) {
|
||||
super(name, owner, keyBindingType);
|
||||
this.plugin = plugin;
|
||||
this.action = action;
|
||||
this.defaultDescription = defaultDescription;
|
||||
}
|
||||
|
||||
private ActionEntry findEntry(ActionContext context) {
|
||||
Target target = plugin.current.getTarget();
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
for (ActionEntry ent : target.collectActions(action, context).values()) {
|
||||
if (ent.requiresPrompt()) {
|
||||
continue;
|
||||
}
|
||||
return ent;
|
||||
// TODO: What if multiple match? Do I care to display the extras?
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void updateFromContext(ActionContext context) {
|
||||
entry = findEntry(context);
|
||||
if (entry == null) {
|
||||
setDescription(defaultDescription);
|
||||
}
|
||||
else {
|
||||
setDescription(entry.details());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabledForContext(ActionContext context) {
|
||||
updateFromContext(context);
|
||||
return entry != null && entry.isEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionContext context) {
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
plugin.runTask(getName(), entry);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import ghidra.debug.api.target.ActionName;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface TargetInterruptAction extends InterruptAction {
|
||||
String NAME = "Interrupt";
|
||||
String DESCRIPTION = "Interrupt the target";
|
||||
String HELP_ANCHOR = "target_interrupt";
|
||||
|
||||
static TargetActionBuilder builder(DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new TargetActionBuilder(NAME, owner)
|
||||
.action(ActionName.INTERRUPT)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.defaultDescription(DESCRIPTION)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.debug.api.target.ActionName;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface TargetKillAction extends ControlAction {
|
||||
String NAME = "Kill";
|
||||
String DESCRIPTION = "Kill the target";
|
||||
Icon ICON = DebuggerResources.ICON_KILL;
|
||||
String HELP_ANCHOR = "target_kill";
|
||||
int SUB_GROUP = 2;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_K,
|
||||
KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK);
|
||||
|
||||
static TargetActionBuilder builder(DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new TargetActionBuilder(NAME, owner)
|
||||
.action(ActionName.KILL)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.defaultDescription(DESCRIPTION)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import ghidra.debug.api.target.ActionName;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface TargetResumeAction extends ResumeAction {
|
||||
String NAME = "Resume";
|
||||
String DESCRIPTION = "Resume the target";
|
||||
String HELP_ANCHOR = "target_resume";
|
||||
|
||||
static TargetActionBuilder builder(DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new TargetActionBuilder(NAME, owner)
|
||||
.action(ActionName.RESUME)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.defaultDescription(DESCRIPTION)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface TargetStepExtAction extends ControlAction {
|
||||
Icon ICON = DebuggerResources.ICON_STEP_LAST;
|
||||
String HELP_ANCHOR = "target_step_ext";
|
||||
int SUB_GROUP = 9;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, KeyEvent.CTRL_DOWN_MASK);
|
||||
|
||||
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(name, ownerName)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP) + name)
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/* ###
|
||||
* 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.control;
|
||||
|
||||
import ghidra.debug.api.target.ActionName;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface TargetStepIntoAction extends StepIntoAction {
|
||||
String NAME = "Step Into";
|
||||
String DESCRIPTION = "Step the target into";
|
||||
String HELP_ANCHOR = "target_step_into";
|
||||
|
||||
static TargetActionBuilder builder(DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new TargetActionBuilder(NAME, owner)
|
||||
.action(ActionName.STEP_INTO)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.defaultDescription(DESCRIPTION)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.control;
|
||||
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.debug.api.target.ActionName;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface TargetStepOutAction extends ControlAction {
|
||||
String NAME = "Step Out";
|
||||
String DESCRIPTION = "Step the target out";
|
||||
Icon ICON = DebuggerResources.ICON_STEP_FINISH;
|
||||
String HELP_ANCHOR = "target_step_out";
|
||||
int SUB_GROUP = 8;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F12, 0);
|
||||
|
||||
static TargetActionBuilder builder(DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new TargetActionBuilder(NAME, owner)
|
||||
.action(ActionName.STEP_OUT)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.defaultDescription(DESCRIPTION)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.control;
|
||||
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.debug.api.target.ActionName;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface TargetStepOverAction extends ControlAction {
|
||||
String NAME = "Step Over";
|
||||
String DESCRIPTION = "Step the target over";
|
||||
Icon ICON = DebuggerResources.ICON_STEP_OVER;
|
||||
String HELP_ANCHOR = "target_step_over";
|
||||
int SUB_GROUP = 6;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F10, 0);
|
||||
|
||||
static TargetActionBuilder builder(DebuggerControlPlugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new TargetActionBuilder(NAME, owner)
|
||||
.action(ActionName.STEP_OVER)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.defaultDescription(DESCRIPTION)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.control;
|
||||
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface TraceSnapBackwardAction extends ControlAction {
|
||||
String NAME = "Trace Snapshot Backward";
|
||||
String DESCRIPTION = "Navigate the trace recording backward one snapshot";
|
||||
Icon ICON = DebuggerResources.ICON_SNAP_BACKWARD;
|
||||
String HELP_ANCHOR = "trace_snap_backward";
|
||||
int SUB_GROUP = 10;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0);
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.control;
|
||||
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
interface TraceSnapForwardAction extends ControlAction {
|
||||
String NAME = "Trace Snapshot Forward";
|
||||
String DESCRIPTION = "Navigate the trace recording forward one snapshot";
|
||||
Icon ICON = DebuggerResources.ICON_SNAP_FORWARD;
|
||||
String HELP_ANCHOR = "trace_snap_backward";
|
||||
int SUB_GROUP = 11;
|
||||
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0);
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
|
||||
.keyBinding(KEY_BINDING)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
|
@ -38,6 +38,7 @@ import docking.ActionContext;
|
|||
import docking.WindowPosition;
|
||||
import docking.action.DockingAction;
|
||||
import docking.action.ToggleDockingAction;
|
||||
import docking.action.builder.ToggleActionBuilder;
|
||||
import docking.menu.MultiStateDockingAction;
|
||||
import docking.widgets.EventTrigger;
|
||||
import docking.widgets.fieldpanel.support.ViewerPosition;
|
||||
|
@ -47,6 +48,7 @@ import ghidra.app.nav.ListingPanelContainer;
|
|||
import ghidra.app.plugin.core.clipboard.CodeBrowserClipboardProvider;
|
||||
import ghidra.app.plugin.core.codebrowser.CodeViewerProvider;
|
||||
import ghidra.app.plugin.core.codebrowser.MarkerServiceBackgroundColorModel;
|
||||
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerLocationLabel;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources.FollowsCurrentThreadAction;
|
||||
|
@ -61,6 +63,8 @@ import ghidra.app.services.*;
|
|||
import ghidra.app.services.DebuggerListingService.LocationTrackingSpecChangeListener;
|
||||
import ghidra.app.util.viewer.format.FormatManager;
|
||||
import ghidra.app.util.viewer.listingpanel.ListingPanel;
|
||||
import ghidra.async.AsyncDebouncer;
|
||||
import ghidra.async.AsyncTimer;
|
||||
import ghidra.debug.api.action.GoToInput;
|
||||
import ghidra.debug.api.action.LocationTrackingSpec;
|
||||
import ghidra.debug.api.control.ControlMode;
|
||||
|
@ -69,11 +73,11 @@ import ghidra.debug.api.modules.DebuggerStaticMappingChangeListener;
|
|||
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.AutoConfigState;
|
||||
import ghidra.framework.plugintool.AutoService;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.annotation.AutoConfigStateField;
|
||||
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
|
||||
import ghidra.program.model.address.*;
|
||||
import ghidra.program.model.listing.Instruction;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.program.util.ProgramSelection;
|
||||
|
@ -114,6 +118,20 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
|||
return true;
|
||||
}
|
||||
|
||||
interface AutoDisassembleAction {
|
||||
String NAME = "Auto-Disassembly";
|
||||
String DESCRIPTION = "If the tracking spec follows the PC, disassemble automatically.";
|
||||
String HELP_ANCHOR = "auto_disassembly";
|
||||
|
||||
static ToggleActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ToggleActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.menuPath(NAME)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
protected class MarkerSetChangeListener implements ChangeListener {
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e) {
|
||||
|
@ -223,6 +241,14 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
|||
@Override
|
||||
protected void locationTracked() {
|
||||
doGoToTracked();
|
||||
if (!autoDisassemble || !trackingTrait.shouldDisassemble()) {
|
||||
return;
|
||||
}
|
||||
disassemblyDebouncer.contact(trackedLocation.getByteAddress());
|
||||
}
|
||||
|
||||
boolean shouldDisassemble() {
|
||||
return trackedLocation != null && tracker.shouldDisassemble();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,6 +267,18 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
|||
protected void repaintPanel() {
|
||||
getListingPanel().getFieldPanel().repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void memoryWasRead(AddressSetView read) {
|
||||
if (!autoDisassemble || !trackingTrait.shouldDisassemble()) {
|
||||
return;
|
||||
}
|
||||
ProgramLocation loc = trackingTrait.getTrackedLocation();
|
||||
if (!read.contains(loc.getByteAddress())) {
|
||||
return;
|
||||
}
|
||||
disassemblyDebouncer.contact(loc.getByteAddress());
|
||||
}
|
||||
}
|
||||
|
||||
private final DebuggerListingPlugin plugin;
|
||||
|
@ -276,6 +314,7 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
|||
protected DockingAction actionSyncSelectionIntoStaticListing;
|
||||
protected DockingAction actionSyncSelectionFromStaticListing;
|
||||
protected ToggleDockingAction actionFollowsCurrentThread;
|
||||
protected ToggleDockingAction actionAutoDisassemble;
|
||||
protected MultiStateDockingAction<AutoReadMemorySpec> actionAutoReadMemory;
|
||||
protected DockingAction actionRefreshSelectedMemory;
|
||||
protected DockingAction actionOpenProgram;
|
||||
|
@ -284,12 +323,17 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
|||
@AutoConfigStateField
|
||||
protected boolean followsCurrentThread = true;
|
||||
// TODO: followsCurrentSnap?
|
||||
@AutoConfigStateField
|
||||
protected boolean autoDisassemble = true;
|
||||
|
||||
protected final ForListingSyncTrait syncTrait;
|
||||
protected final ForListingGoToTrait goToTrait;
|
||||
protected final ForListingTrackingTrait trackingTrait;
|
||||
protected final ForListingReadsMemoryTrait readsMemTrait;
|
||||
|
||||
protected final AsyncDebouncer<Address> disassemblyDebouncer =
|
||||
new AsyncDebouncer<>(AsyncTimer.DEFAULT_TIMER, 100);
|
||||
|
||||
protected final ListenerSet<LocationTrackingSpecChangeListener> trackingSpecChangeListeners =
|
||||
new ListenerSet<>(LocationTrackingSpecChangeListener.class, true);
|
||||
|
||||
|
@ -322,6 +366,8 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
|||
trackingTrait = new ForListingTrackingTrait();
|
||||
readsMemTrait = new ForListingReadsMemoryTrait();
|
||||
|
||||
disassemblyDebouncer.addListener(this::doAutoDisassemble);
|
||||
|
||||
ListingPanel listingPanel = getListingPanel();
|
||||
colorModel = plugin.createListingBackgroundColorModel(listingPanel);
|
||||
colorModel.addModel(trackingTrait.createListingBackgroundColorModel(listingPanel));
|
||||
|
@ -467,6 +513,7 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
|||
actionFollowsCurrentThread.setSelected(followsCurrentThread);
|
||||
updateBorder();
|
||||
}
|
||||
actionAutoDisassemble.setSelected(autoDisassemble);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -705,6 +752,12 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
|||
.buildAndInstallLocal(this);
|
||||
}
|
||||
|
||||
actionAutoDisassemble = AutoDisassembleAction.builder(plugin)
|
||||
.enabled(true)
|
||||
.selected(true)
|
||||
.onAction(ctx -> doSetAutoDisassemble(actionAutoDisassemble.isSelected()))
|
||||
.buildAndInstallLocal(this);
|
||||
|
||||
actionSyncSelectionIntoStaticListing =
|
||||
syncTrait.installSyncSelectionIntoStaticListingAction();
|
||||
actionSyncSelectionFromStaticListing =
|
||||
|
@ -757,17 +810,16 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
|||
* which applies to many things in general. This one is for changes in the listing model's
|
||||
* "size", i.e., the memory mapping or assigned view has changed. This should be the perfect
|
||||
* place to ensure the tracked location is centered, if applicable.
|
||||
*
|
||||
* <p>
|
||||
* It seems this method gets called a bit spuriously. A change in bytes, which does not imply a
|
||||
* change in layout, will also land us here. Thus, we do some simple test here to verify that
|
||||
* the layout has actually changed. A good proxy is if the number of addresses in the listing
|
||||
* has changed. To detect that, we have to record what we've seen each change.
|
||||
*/
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e) {
|
||||
super.stateChanged(e);
|
||||
/*
|
||||
* It seems this method gets called a bit spuriously. A change in bytes, which does not
|
||||
* imply a change in layout, will also land us here. Thus, we do some simple test here to
|
||||
* verify that the layout has actually changed. A good proxy is if the number of addresses
|
||||
* in the listing has changed. To detect that, we have to record what we've seen each
|
||||
* change.
|
||||
*/
|
||||
long newCountAddressesInIndex =
|
||||
getListingPanel().getAddressIndexMap().getIndexedAddressSet().getNumAddresses();
|
||||
if (this.countAddressesInIndex == newCountAddressesInIndex) {
|
||||
|
@ -776,7 +828,12 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
|||
this.countAddressesInIndex = newCountAddressesInIndex;
|
||||
ProgramLocation trackedLocation = trackingTrait.getTrackedLocation();
|
||||
if (trackedLocation != null && !isEffectivelyDifferent(getLocation(), trackedLocation)) {
|
||||
cbGoTo.invoke(() -> getListingPanel().goTo(trackedLocation, true));
|
||||
cbGoTo.invoke(() -> Swing.runLater(() -> {
|
||||
boolean goneTo = getListingPanel().goTo(trackedLocation, true);
|
||||
if (goneTo) {
|
||||
getListingPanel().center(trackedLocation);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1037,6 +1094,15 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
|||
coordinatesActivated(traceManager.getCurrent());
|
||||
}
|
||||
|
||||
public void setAutoDisassemble(boolean auto) {
|
||||
actionAutoDisassemble.setSelected(true);
|
||||
doSetAutoDisassemble(auto);
|
||||
}
|
||||
|
||||
protected void doSetAutoDisassemble(boolean auto) {
|
||||
this.autoDisassemble = auto;
|
||||
}
|
||||
|
||||
protected void updateBorder() {
|
||||
// TODO: Probably make this accessible from abstract class, instead
|
||||
ListingPanelContainer decoration = (ListingPanelContainer) getComponent();
|
||||
|
@ -1047,6 +1113,10 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
|||
return followsCurrentThread;
|
||||
}
|
||||
|
||||
public boolean isAutoDisassemble() {
|
||||
return autoDisassemble;
|
||||
}
|
||||
|
||||
public void setAutoReadMemorySpec(AutoReadMemorySpec spec) {
|
||||
readsMemTrait.setAutoSpec(spec);
|
||||
}
|
||||
|
@ -1113,6 +1183,27 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
|||
}
|
||||
}
|
||||
|
||||
protected void doAutoDisassemble(Address start) {
|
||||
TraceProgramView view = current.getView();
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* We'll avoid re-disassembly only if there already exists an instruction <em>at the start
|
||||
* address</em>. If it's in the middle, then we're off cut and should re-disassemble at the
|
||||
* new start.
|
||||
*/
|
||||
Instruction exists = view.getListing().getInstructionAt(start);
|
||||
if (exists != null) {
|
||||
return;
|
||||
}
|
||||
AddressSpace space = start.getAddressSpace();
|
||||
AddressSet set = new AddressSet(space.getMinAddress(), space.getMaxAddress());
|
||||
TraceDisassembleCommand dis =
|
||||
new TraceDisassembleCommand(current.getPlatform(), start, set);
|
||||
dis.run(tool, view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
|
|
|
@ -115,7 +115,7 @@ public abstract class AbstractObjectsTableBasedPanel<U extends TraceObjectInterf
|
|||
List<ValueRow> sel = getSelectedItems();
|
||||
if (!sel.isEmpty()) {
|
||||
myActionContext = new DebuggerObjectActionContext(
|
||||
sel.stream().map(r -> r.getValue()).collect(Collectors.toList()), provider, this);
|
||||
sel.stream().map(r -> r.getValue()).collect(Collectors.toList()), provider, table);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,10 +15,15 @@
|
|||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.model;
|
||||
|
||||
import java.util.stream.*;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.trace.model.target.TraceObject;
|
||||
import ghidra.trace.model.target.TraceObjectValue;
|
||||
import ghidra.util.HTMLUtilities;
|
||||
import ghidra.util.NumericUtilities;
|
||||
|
||||
public interface DisplaysObjectValues {
|
||||
long getSnap();
|
||||
|
@ -27,6 +32,38 @@ public interface DisplaysObjectValues {
|
|||
return "";
|
||||
}
|
||||
|
||||
default String getBoolsDisplay(boolean[] bools) {
|
||||
return Stream.of(ArrayUtils.toObject(bools))
|
||||
.map(b -> b ? "T" : "F")
|
||||
.collect(Collectors.joining(":"));
|
||||
}
|
||||
|
||||
default String getBytesDisplay(byte[] bytes) {
|
||||
return NumericUtilities.convertBytesToString(bytes, ":");
|
||||
}
|
||||
|
||||
default String getCharsDisplay(char[] chars) {
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
default String getShortsDisplay(short[] shorts) {
|
||||
return Stream.of(ArrayUtils.toObject(shorts))
|
||||
.map(s -> "%04x".formatted(s))
|
||||
.collect(Collectors.joining(":"));
|
||||
}
|
||||
|
||||
default String getIntsDisplay(int[] ints) {
|
||||
return IntStream.of(ints)
|
||||
.mapToObj(i -> "%08x".formatted(i))
|
||||
.collect(Collectors.joining(":"));
|
||||
}
|
||||
|
||||
default String getLongsDisplay(long[] longs) {
|
||||
return LongStream.of(longs)
|
||||
.mapToObj(l -> "%016x".formatted(l))
|
||||
.collect(Collectors.joining(":"));
|
||||
}
|
||||
|
||||
default String getPrimitiveValueDisplay(Object value) {
|
||||
assert !(value instanceof TraceObject);
|
||||
assert !(value instanceof TraceObjectValue);
|
||||
|
@ -34,6 +71,24 @@ public interface DisplaysObjectValues {
|
|||
if (value == null) {
|
||||
return getNullDisplay();
|
||||
}
|
||||
if (value instanceof boolean[] bools) {
|
||||
return getBoolsDisplay(bools);
|
||||
}
|
||||
if (value instanceof byte[] bytes) {
|
||||
return getBytesDisplay(bytes);
|
||||
}
|
||||
if (value instanceof char[] chars) {
|
||||
return getCharsDisplay(chars);
|
||||
}
|
||||
if (value instanceof short[] shorts) {
|
||||
return getShortsDisplay(shorts);
|
||||
}
|
||||
if (value instanceof int[] ints) {
|
||||
return getIntsDisplay(ints);
|
||||
}
|
||||
if (value instanceof long[] longs) {
|
||||
return getLongsDisplay(longs);
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
|
|
|
@ -17,8 +17,9 @@ package ghidra.app.plugin.core.debug.gui.model;
|
|||
|
||||
import java.awt.Color;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.*;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
import docking.widgets.table.DynamicTableColumn;
|
||||
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
|
||||
|
@ -38,6 +39,7 @@ import ghidra.trace.model.Trace;
|
|||
import ghidra.trace.model.target.TraceObject;
|
||||
import ghidra.trace.model.target.TraceObjectValue;
|
||||
import ghidra.util.HTMLUtilities;
|
||||
import ghidra.util.NumericUtilities;
|
||||
|
||||
public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
|
||||
|
||||
|
@ -61,7 +63,36 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
|
|||
|
||||
default public String getDisplay() {
|
||||
T value = getValue();
|
||||
return value == null ? "" : value.toString();
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
if (value instanceof boolean[] bools) {
|
||||
return Stream.of(ArrayUtils.toObject(bools))
|
||||
.map(b -> b ? "T" : "F")
|
||||
.collect(Collectors.joining(":"));
|
||||
}
|
||||
if (value instanceof byte[] bytes) {
|
||||
return NumericUtilities.convertBytesToString(bytes, ":");
|
||||
}
|
||||
if (value instanceof char[] chars) {
|
||||
return new String(chars);
|
||||
}
|
||||
if (value instanceof short[] shorts) {
|
||||
return Stream.of(ArrayUtils.toObject(shorts))
|
||||
.map(s -> "%04x".formatted(s))
|
||||
.collect(Collectors.joining(":"));
|
||||
}
|
||||
if (value instanceof int[] ints) {
|
||||
return IntStream.of(ints)
|
||||
.mapToObj(i -> "%08x".formatted(i))
|
||||
.collect(Collectors.joining(":"));
|
||||
}
|
||||
if (value instanceof long[] longs) {
|
||||
return LongStream.of(longs)
|
||||
.mapToObj(l -> "%016x".formatted(l))
|
||||
.collect(Collectors.joining(":"));
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
default public String getHtmlDisplay() {
|
||||
|
|
|
@ -21,6 +21,7 @@ import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin;
|
|||
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
|
||||
import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
|
||||
|
@ -81,4 +82,14 @@ public class DebuggerModulesPlugin extends AbstractDebuggerPlugin {
|
|||
provider.coordinatesActivated(ev.getActiveCoordinates());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readConfigState(SaveState saveState) {
|
||||
provider.readConfigState(saveState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeConfigState(SaveState saveState) {
|
||||
provider.writeConfigState(saveState);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ package ghidra.app.plugin.core.debug.gui.modules;
|
|||
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.io.File;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -28,12 +29,20 @@ import org.apache.commons.lang3.ArrayUtils;
|
|||
import docking.*;
|
||||
import docking.action.*;
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import docking.action.builder.MultiStateActionBuilder;
|
||||
import docking.menu.ActionState;
|
||||
import docking.menu.MultiStateDockingAction;
|
||||
import docking.widgets.EventTrigger;
|
||||
import docking.widgets.filechooser.GhidraFileChooser;
|
||||
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerBlockChooserDialog;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources.*;
|
||||
import ghidra.app.plugin.core.debug.gui.action.AutoMapSpec;
|
||||
import ghidra.app.plugin.core.debug.gui.action.AutoMapSpec.AutoMapSpecConfigFieldCodec;
|
||||
import ghidra.app.plugin.core.debug.gui.action.ByModuleAutoMapSpec;
|
||||
import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext;
|
||||
import ghidra.app.plugin.core.debug.service.model.TraceRecorderTarget;
|
||||
import ghidra.app.plugin.core.debug.service.modules.MapModulesBackgroundCommand;
|
||||
import ghidra.app.plugin.core.debug.service.modules.MapSectionsBackgroundCommand;
|
||||
import ghidra.app.services.*;
|
||||
|
@ -44,7 +53,9 @@ import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
|||
import ghidra.framework.main.AppInfo;
|
||||
import ghidra.framework.main.DataTreeDialog;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.annotation.AutoConfigStateField;
|
||||
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
|
||||
import ghidra.framework.plugintool.util.PluginException;
|
||||
import ghidra.program.model.address.*;
|
||||
|
@ -53,11 +64,15 @@ import ghidra.program.model.mem.MemoryBlock;
|
|||
import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.program.util.ProgramSelection;
|
||||
import ghidra.trace.model.*;
|
||||
import ghidra.trace.model.Trace.TraceMemoryBytesChangeType;
|
||||
import ghidra.trace.model.modules.*;
|
||||
import ghidra.trace.util.TraceChangeType;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
protected static final AutoConfigState.ClassHandler<DebuggerModulesProvider> CONFIG_STATE_HANDLER =
|
||||
AutoConfigState.wireHandler(DebuggerModulesProvider.class, MethodHandles.lookup());
|
||||
|
||||
protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) {
|
||||
if (!Objects.equals(a.getTrace(), b.getTrace())) {
|
||||
|
@ -179,6 +194,53 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
interface AutoMapAction {
|
||||
String NAME = "Auto-Map Target Memory";
|
||||
Icon ICON = DebuggerResources.ICON_CONFIG;
|
||||
String DESCRIPTION = "Automatically map dynamic memory to static counterparts";
|
||||
String GROUP = DebuggerResources.GROUP_MAPPING;
|
||||
String HELP_ANCHOR = "auto_map";
|
||||
|
||||
static MultiStateActionBuilder<AutoMapSpec> builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
MultiStateActionBuilder<AutoMapSpec> builder =
|
||||
new MultiStateActionBuilder<AutoMapSpec>(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarGroup(GROUP)
|
||||
.toolBarIcon(ICON)
|
||||
.useCheckboxForIcons(true)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
for (AutoMapSpec spec : AutoMapSpec.allSpecs().values()) {
|
||||
builder.addState(spec.getMenuName(), spec.getMenuIcon(), spec);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
protected class ForMappingTraceListener extends TraceDomainObjectListener {
|
||||
public ForMappingTraceListener(AutoMapSpec spec) {
|
||||
for (TraceChangeType<?, ?> type : spec.getChangeTypes()) {
|
||||
listenFor(type, this::changed);
|
||||
}
|
||||
|
||||
// TODO: Delete this if/when TraceRecorderTarget is removed
|
||||
listenFor(TraceMemoryBytesChangeType.CHANGED, this::memoryChanged);
|
||||
}
|
||||
|
||||
private void changed() {
|
||||
cueAutoMap = true;
|
||||
}
|
||||
|
||||
private void memoryChanged(TraceAddressSnapRange range) {
|
||||
if (range.getRange().getAddressSpace().isRegisterSpace()) {
|
||||
return;
|
||||
}
|
||||
if (current.getTarget() instanceof TraceRecorderTarget) {
|
||||
doCuedAutoMap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static Set<TraceModule> getSelectedModules(ActionContext context) {
|
||||
if (context instanceof DebuggerModuleActionContext ctx) {
|
||||
return DebuggerLegacyModulesPanel.getSelectedModulesFromContext(ctx);
|
||||
|
@ -332,6 +394,14 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
|||
DockingAction actionMapSectionTo;
|
||||
DockingAction actionMapSectionsTo;
|
||||
|
||||
MultiStateDockingAction<AutoMapSpec> actionAutoMap;
|
||||
private final AutoMapSpec defaultAutoMapSpec =
|
||||
AutoMapSpec.fromConfigName(ByModuleAutoMapSpec.CONFIG_NAME);
|
||||
@AutoConfigStateField(codec = AutoMapSpecConfigFieldCodec.class)
|
||||
AutoMapSpec autoMapSpec = defaultAutoMapSpec;
|
||||
boolean cueAutoMap;
|
||||
private ForMappingTraceListener forMappingListener;
|
||||
|
||||
DockingAction actionImportMissingModule;
|
||||
DockingAction actionMapMissingModule;
|
||||
|
||||
|
@ -492,6 +562,11 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
|||
.onAction(this::activatedMapSectionsTo)
|
||||
.buildAndInstallLocal(this);
|
||||
|
||||
actionAutoMap = AutoMapAction.builder(plugin)
|
||||
.onActionStateChanged(this::changedAutoMapSpec)
|
||||
.buildAndInstallLocal(this);
|
||||
actionAutoMap.setCurrentActionStateByUserData(defaultAutoMapSpec);
|
||||
|
||||
actionImportMissingModule = ImportMissingModuleAction.builder(plugin)
|
||||
.withContext(DebuggerMissingModuleActionContext.class)
|
||||
.onAction(this::activatedImportMissingModule)
|
||||
|
@ -535,7 +610,13 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
|||
if (sel == null || sel.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return sel.stream().map(TraceSection::getModule).distinct().count() == 1;
|
||||
try {
|
||||
return sel.stream().map(TraceSection::getModule).distinct().count() == 1;
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Could not check section selection context: " + e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void activatedMapIdentically(ActionContext ignored) {
|
||||
|
@ -544,7 +625,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
|||
}
|
||||
try {
|
||||
staticMappingService.addIdentityMapping(current.getTrace(), currentProgram,
|
||||
Lifespan.nowOn(traceManager.getCurrentSnap()), false);
|
||||
Lifespan.nowOn(traceManager.getCurrentSnap()), true);
|
||||
}
|
||||
catch (TraceConflictedMappingException e) {
|
||||
Msg.showError(this, null, "Map Identically", e.getMessage());
|
||||
|
@ -611,6 +692,19 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
|||
mapSectionTo(sel.iterator().next());
|
||||
}
|
||||
|
||||
private void changedAutoMapSpec(ActionState<AutoMapSpec> newState, EventTrigger trigger) {
|
||||
doSetAutoMapSpec(newState.getUserData());
|
||||
}
|
||||
|
||||
private void doSetAutoMapSpec(AutoMapSpec autoMapSpec) {
|
||||
if (this.autoMapSpec == autoMapSpec) {
|
||||
return;
|
||||
}
|
||||
removeOldTraceListener();
|
||||
this.autoMapSpec = autoMapSpec;
|
||||
addNewTraceListener();
|
||||
}
|
||||
|
||||
private void activatedImportMissingModule(DebuggerMissingModuleActionContext context) {
|
||||
if (importerService == null) {
|
||||
Msg.error(this, "Import service is not present");
|
||||
|
@ -877,13 +971,37 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
protected void addNewTraceListener() {
|
||||
if (current.getTrace() != null && autoMapSpec != null) {
|
||||
forMappingListener = new ForMappingTraceListener(autoMapSpec);
|
||||
current.getTrace().addListener(forMappingListener);
|
||||
}
|
||||
}
|
||||
|
||||
protected void removeOldTraceListener() {
|
||||
if (forMappingListener != null) {
|
||||
if (current.getTrace() != null) {
|
||||
current.getTrace().removeListener(forMappingListener);
|
||||
}
|
||||
forMappingListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void coordinatesActivated(DebuggerCoordinates coordinates) {
|
||||
if (sameCoordinates(current, coordinates)) {
|
||||
current = coordinates;
|
||||
return;
|
||||
}
|
||||
|
||||
boolean changeTrace = current.getTrace() != coordinates.getTrace();
|
||||
if (changeTrace) {
|
||||
myActionContext = null;
|
||||
removeOldTraceListener();
|
||||
}
|
||||
current = coordinates;
|
||||
if (changeTrace) {
|
||||
addNewTraceListener();
|
||||
}
|
||||
|
||||
if (Trace.isLegacy(coordinates.getTrace())) {
|
||||
modulesPanel.coordinatesActivated(DebuggerCoordinates.NOWHERE);
|
||||
|
@ -913,6 +1031,22 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
|||
}
|
||||
|
||||
contextChanged();
|
||||
|
||||
if (coordinates.getTarget() instanceof TraceRecorderTarget) {
|
||||
// HACK while TraceRecorderTarget is still around
|
||||
cueAutoMap = true;
|
||||
}
|
||||
doCuedAutoMap();
|
||||
}
|
||||
|
||||
private void doCuedAutoMap() {
|
||||
if (cueAutoMap) {
|
||||
cueAutoMap = false;
|
||||
Trace trace = current.getTrace();
|
||||
if (autoMapSpec != null && trace != null) {
|
||||
autoMapSpec.runTask(tool, trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelectedModules(Set<TraceModule> sel) {
|
||||
|
@ -963,4 +1097,21 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
|||
return blockChooserDialog.chooseBlock(getTool(), section,
|
||||
List.of(programManager.getAllOpenPrograms()));
|
||||
}
|
||||
|
||||
public void setAutoMapSpec(AutoMapSpec spec) {
|
||||
actionAutoMap.setCurrentActionStateByUserData(spec);
|
||||
}
|
||||
|
||||
public AutoMapSpec getAutoMapSpec() {
|
||||
return autoMapSpec;
|
||||
}
|
||||
|
||||
public void writeConfigState(SaveState saveState) {
|
||||
CONFIG_STATE_HANDLER.writeConfigState(this, saveState);
|
||||
}
|
||||
|
||||
public void readConfigState(SaveState saveState) {
|
||||
CONFIG_STATE_HANDLER.readConfigState(this, saveState);
|
||||
actionAutoMap.setCurrentActionStateByUserData(autoMapSpec);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,11 @@ package ghidra.app.plugin.core.debug.gui.thread;
|
|||
import java.util.List;
|
||||
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
|
||||
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
|
||||
import docking.widgets.table.threaded.ThreadedTableModelListener;
|
||||
import docking.widgets.table.TableColumnDescriptor;
|
||||
import docking.widgets.table.threaded.ThreadedTableModelListener;
|
||||
import ghidra.app.plugin.core.debug.gui.model.*;
|
||||
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.*;
|
||||
import ghidra.app.plugin.core.debug.gui.model.columns.*;
|
||||
|
@ -165,6 +166,8 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
|
|||
@AutoServiceConsumed
|
||||
protected DebuggerTraceManagerService traceManager;
|
||||
|
||||
private final DebuggerThreadsProvider provider;
|
||||
|
||||
private final SeekListener seekListener = pos -> {
|
||||
long snap = Math.round(pos);
|
||||
if (current.getTrace() == null || snap < 0) {
|
||||
|
@ -175,6 +178,7 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
|
|||
|
||||
public DebuggerThreadsPanel(DebuggerThreadsProvider provider) {
|
||||
super(provider.plugin, provider, TraceObjectThread.class);
|
||||
this.provider = provider;
|
||||
setLimitToSnap(false); // TODO: Toggle for this?
|
||||
|
||||
addSeekListener(seekListener);
|
||||
|
@ -239,4 +243,13 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
|
|||
traceManager.activateObject(item.getValue().getChild());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void valueChanged(ListSelectionEvent e) {
|
||||
super.valueChanged(e);
|
||||
if (e.getValueIsAdjusting()) {
|
||||
return;
|
||||
}
|
||||
provider.threadsPanelContextChanged();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -207,6 +207,10 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
|
|||
super.addLocalAction(action);
|
||||
}
|
||||
|
||||
void threadsPanelContextChanged() {
|
||||
myActionContext = panel.getActionContext();
|
||||
}
|
||||
|
||||
void legacyThreadsPanelContextChanged() {
|
||||
myActionContext = legacyPanel.getActionContext();
|
||||
}
|
||||
|
|
|
@ -192,10 +192,14 @@ public class DebuggerTraceTabPanel extends HorizontalTabPanel<Trace>
|
|||
return;
|
||||
}
|
||||
if (event instanceof TraceOpenedPluginEvent evt) {
|
||||
addItem(evt.getTrace());
|
||||
try (Suppression supp = cbCoordinateActivation.suppress(null)) {
|
||||
addItem(evt.getTrace());
|
||||
}
|
||||
}
|
||||
else if (event instanceof TraceClosedPluginEvent evt) {
|
||||
removeItem(evt.getTrace());
|
||||
try (Suppression supp = cbCoordinateActivation.suppress(null)) {
|
||||
removeItem(evt.getTrace());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,6 +208,8 @@ public class DebuggerTraceTabPanel extends HorizontalTabPanel<Trace>
|
|||
return;
|
||||
}
|
||||
Trace newTrace = setTraceTabActionContext(null);
|
||||
cbCoordinateActivation.invoke(() -> traceManager.activateTrace(newTrace));
|
||||
cbCoordinateActivation.invoke(() -> {
|
||||
traceManager.activateTrace(newTrace);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import ghidra.app.plugin.PluginCategoryNames;
|
|||
import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin;
|
||||
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.*;
|
||||
import ghidra.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.*;
|
||||
|
@ -32,6 +33,7 @@ import ghidra.framework.plugintool.util.PluginStatus;
|
|||
status = PluginStatus.RELEASED,
|
||||
eventsConsumed = {
|
||||
TraceActivatedPluginEvent.class,
|
||||
TraceClosedPluginEvent.class,
|
||||
},
|
||||
servicesProvided = {
|
||||
DebuggerWatchesService.class,
|
||||
|
@ -64,10 +66,13 @@ public class DebuggerWatchesPlugin extends AbstractDebuggerPlugin {
|
|||
@Override
|
||||
public void processEvent(PluginEvent event) {
|
||||
super.processEvent(event);
|
||||
if (event instanceof TraceActivatedPluginEvent) {
|
||||
TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event;
|
||||
if (event instanceof TraceActivatedPluginEvent ev) {
|
||||
provider.coordinatesActivated(ev.getActiveCoordinates());
|
||||
}
|
||||
else if (event instanceof TraceClosedPluginEvent ev) {
|
||||
// Activation not good enough. Need to know if "previous" was closed
|
||||
provider.traceClosed(ev.getTrace());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -875,14 +875,34 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter
|
|||
language = null;
|
||||
}
|
||||
|
||||
asyncWatchExecutor = current.getPlatform() == null ? null
|
||||
: DebuggerPcodeUtils.buildWatchExecutor(tool, current);
|
||||
prevValueExecutor = current.getPlatform() == null || previous.getPlatform() == null ? null
|
||||
: TraceSleighUtils.buildByteExecutor(previous.getPlatform(),
|
||||
previous.getViewSnap(), previous.getThread(), previous.getFrame());
|
||||
try {
|
||||
asyncWatchExecutor = current.getPlatform() == null ? null
|
||||
: DebuggerPcodeUtils.buildWatchExecutor(tool, current);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Error constructing watch executor: " + e);
|
||||
asyncWatchExecutor = null;
|
||||
}
|
||||
try {
|
||||
prevValueExecutor =
|
||||
current.getPlatform() == null || previous.getPlatform() == null ? null
|
||||
: TraceSleighUtils.buildByteExecutor(previous.getPlatform(),
|
||||
previous.getViewSnap(), previous.getThread(), previous.getFrame());
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Error constructing previous-value executor: " + e);
|
||||
prevValueExecutor = null;
|
||||
}
|
||||
reevaluate();
|
||||
}
|
||||
|
||||
public void traceClosed(Trace trace) {
|
||||
if (previous.getTrace() == trace) {
|
||||
previous = DebuggerCoordinates.NOWHERE;
|
||||
prevValueExecutor = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void clearCachedState() {
|
||||
if (asyncWatchExecutor != null) {
|
||||
asyncWatchExecutor.getState().clear();
|
||||
|
|
|
@ -18,8 +18,8 @@ package ghidra.app.plugin.core.debug.mapping;
|
|||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
import ghidra.app.plugin.core.debug.disassemble.DisassemblyInject;
|
||||
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
|
||||
import ghidra.app.plugin.core.debug.workflow.DisassemblyInject;
|
||||
import ghidra.debug.api.platform.DebuggerPlatformMapper;
|
||||
import ghidra.debug.api.platform.DisassemblyResult;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
|
|
|
@ -18,8 +18,8 @@ package ghidra.app.plugin.core.debug.mapping.legacy;
|
|||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ghidra.app.plugin.core.debug.disassemble.DisassemblyInject;
|
||||
import ghidra.app.plugin.core.debug.mapping.*;
|
||||
import ghidra.app.plugin.core.debug.workflow.DisassemblyInject;
|
||||
import ghidra.debug.api.platform.DebuggerPlatformMapper;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.lifecycle.Transitional;
|
||||
|
|
|
@ -17,10 +17,8 @@ package ghidra.app.plugin.core.debug.platform.arm;
|
|||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
|
||||
import ghidra.app.plugin.core.debug.workflow.DisassemblyInject;
|
||||
import ghidra.app.plugin.core.debug.workflow.DisassemblyInjectInfo;
|
||||
import ghidra.app.plugin.core.debug.workflow.DisassemblyInjectInfo.CompilerInfo;
|
||||
import ghidra.app.plugin.core.debug.disassemble.*;
|
||||
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.*;
|
||||
|
|
|
@ -18,8 +18,8 @@ package ghidra.app.plugin.core.debug.platform.dbgeng;
|
|||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
import ghidra.app.plugin.core.debug.disassemble.DisassemblyInject;
|
||||
import ghidra.app.plugin.core.debug.mapping.*;
|
||||
import ghidra.app.plugin.core.debug.workflow.DisassemblyInject;
|
||||
import ghidra.debug.api.platform.DebuggerPlatformMapper;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.lang.*;
|
||||
|
|
|
@ -21,10 +21,8 @@ import java.util.Collection;
|
|||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
|
||||
import ghidra.app.plugin.core.debug.workflow.DisassemblyInject;
|
||||
import ghidra.app.plugin.core.debug.workflow.DisassemblyInjectInfo;
|
||||
import ghidra.app.plugin.core.debug.workflow.DisassemblyInjectInfo.CompilerInfo;
|
||||
import ghidra.app.plugin.core.debug.disassemble.*;
|
||||
import ghidra.app.plugin.core.debug.disassemble.DisassemblyInjectInfo.CompilerInfo;
|
||||
import ghidra.app.services.DebuggerTargetService;
|
||||
import ghidra.app.util.bin.ByteProvider;
|
||||
import ghidra.app.util.bin.MemBufferByteProvider;
|
||||
|
|
|
@ -228,7 +228,8 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin
|
|||
}
|
||||
|
||||
private void breakpointAdded(TraceBreakpoint tb) {
|
||||
if (!tb.getLifespan().contains(info.snap)) {
|
||||
Lifespan span = tb.getLifespan();
|
||||
if (span == null || !span.contains(info.snap)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
@ -600,6 +601,9 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin
|
|||
return;
|
||||
}
|
||||
Address traceAddr = tb.getMinAddress();
|
||||
if (traceAddr == null) {
|
||||
return; // Will update via breakpointChanged when address is set
|
||||
}
|
||||
ProgramLocation progLoc = computeStaticLocation(tb);
|
||||
LogicalBreakpointInternal lb;
|
||||
if (progLoc != null) {
|
||||
|
@ -1112,7 +1116,11 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin
|
|||
public LogicalBreakpoint getBreakpoint(TraceBreakpoint bpt) {
|
||||
Trace trace = bpt.getTrace();
|
||||
synchronized (lock) {
|
||||
for (LogicalBreakpoint lb : getBreakpointsAt(trace, bpt.getMinAddress())) {
|
||||
Address address = bpt.getMinAddress();
|
||||
if (address == null) {
|
||||
return null;
|
||||
}
|
||||
for (LogicalBreakpoint lb : getBreakpointsAt(trace, address)) {
|
||||
if (lb.getTraceBreakpoints(trace).contains(bpt)) {
|
||||
return lb;
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ import java.util.concurrent.CompletableFuture;
|
|||
|
||||
import db.Transaction;
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.target.TargetBreakpointSpec;
|
||||
import ghidra.dbg.target.TargetBreakpointSpecContainer;
|
||||
import ghidra.dbg.target.*;
|
||||
import ghidra.dbg.util.PathMatcher;
|
||||
import ghidra.dbg.util.PathPattern;
|
||||
import ghidra.program.model.address.*;
|
||||
import ghidra.trace.model.Lifespan;
|
||||
import ghidra.trace.model.Trace;
|
||||
|
@ -88,11 +88,21 @@ public record PlaceEmuBreakpointActionItem(Trace trace, long snap, Address addre
|
|||
if (specMatcher == null) {
|
||||
throw new IllegalArgumentException("Cannot find path to breakpoint specifications");
|
||||
}
|
||||
List<String> relPath = specMatcher.applyKeys(name).getSingletonPath();
|
||||
if (relPath == null) {
|
||||
List<String> specRelPath = specMatcher.applyKeys(name).getSingletonPath();
|
||||
if (specRelPath == null) {
|
||||
throw new IllegalArgumentException("Too many wildcards to breakpoint specification");
|
||||
}
|
||||
return container.getCanonicalPath().extend(relPath).toString();
|
||||
PathMatcher locMatcher = container.getTargetSchema()
|
||||
.getSuccessorSchema(specRelPath)
|
||||
.searchFor(TargetBreakpointLocation.class, true);
|
||||
if (locMatcher == null) {
|
||||
throw new IllegalArgumentException("Cannot find path to breakpoint locations");
|
||||
}
|
||||
List<String> locRelPath = locMatcher.applyIntKeys(0).getSingletonPath();
|
||||
if (locRelPath == null) {
|
||||
throw new IllegalArgumentException("Too many wildcards to breakpoint location");
|
||||
}
|
||||
return container.getCanonicalPath().extend(specRelPath).extend(locRelPath).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -24,6 +24,7 @@ import java.util.function.Predicate;
|
|||
import docking.ActionContext;
|
||||
import ghidra.app.context.ProgramLocationActionContext;
|
||||
import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext;
|
||||
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
|
||||
import ghidra.app.plugin.core.debug.service.target.AbstractTarget;
|
||||
import ghidra.app.services.DebuggerConsoleService;
|
||||
import ghidra.app.services.DebuggerTraceManagerService;
|
||||
|
@ -176,6 +177,14 @@ public class TraceRecorderTarget extends AbstractTarget {
|
|||
}
|
||||
|
||||
private record MethodWithArgs(TargetMethod method, Map<String, Object> arguments) {
|
||||
public boolean requiresPrompt() {
|
||||
for (ParameterDescription<?> param : method.getParameters().values()) {
|
||||
if (param.required && !arguments.containsKey(param.name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<MethodWithArgs> findAddressMethods(ProgramLocationActionContext context) {
|
||||
|
@ -215,23 +224,59 @@ public class TraceRecorderTarget extends AbstractTarget {
|
|||
return method.getName();
|
||||
}
|
||||
|
||||
private ActionEntry makeEntry(TargetMethod method, Map<String, ?> arguments) {
|
||||
return new ActionEntry(method.getDisplay(), null, null, false, () -> true, () -> {
|
||||
return method.invoke(arguments).thenAccept(result -> {
|
||||
DebuggerConsoleService consoleService =
|
||||
tool.getService(DebuggerConsoleService.class);
|
||||
if (consoleService != null && method.getReturnType() != Void.class) {
|
||||
consoleService.log(null, getDisplay(method) + " returned " + result);
|
||||
private Map<String, ?> promptArgs(TargetMethod method, Map<String, ?> defaults) {
|
||||
DebuggerMethodInvocationDialog dialog = new DebuggerMethodInvocationDialog(tool,
|
||||
method.getDisplay(), method.getDisplay(), null);
|
||||
while (true) {
|
||||
for (ParameterDescription<?> param : method.getParameters().values()) {
|
||||
Object val = defaults.get(param.name);
|
||||
if (val != null) {
|
||||
dialog.setMemorizedArgument(param.name, param.type.asSubclass(Object.class),
|
||||
val);
|
||||
}
|
||||
});
|
||||
}
|
||||
Map<String, ?> args = dialog.promptArguments(method.getParameters());
|
||||
if (args == null) {
|
||||
// Cancelled
|
||||
return null;
|
||||
}
|
||||
if (dialog.isResetRequested()) {
|
||||
continue;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<?> invokeMethod(boolean prompt, TargetMethod method,
|
||||
Map<String, ?> arguments) {
|
||||
Map<String, ?> chosenArgs;
|
||||
if (prompt) {
|
||||
chosenArgs = promptArgs(method, arguments);
|
||||
}
|
||||
else {
|
||||
chosenArgs = arguments;
|
||||
}
|
||||
return method.invoke(chosenArgs).thenAccept(result -> {
|
||||
DebuggerConsoleService consoleService =
|
||||
tool.getService(DebuggerConsoleService.class);
|
||||
if (consoleService != null && method.getReturnType() != Void.class) {
|
||||
consoleService.log(null, getDisplay(method) + " returned " + result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ActionEntry makeEntry(boolean requiresPrompt, TargetMethod method,
|
||||
Map<String, ?> arguments) {
|
||||
return new ActionEntry(method.getDisplay(), null, null, requiresPrompt, () -> true,
|
||||
prompt -> invokeMethod(prompt, method, arguments));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, ActionEntry> collectAddressActions(ProgramLocationActionContext context) {
|
||||
Map<String, ActionEntry> result = new HashMap<>();
|
||||
for (MethodWithArgs mwa : findAddressMethods(context)) {
|
||||
result.put(mwa.method.getJoinedPath("."), makeEntry(mwa.method, mwa.arguments));
|
||||
result.put(mwa.method.getJoinedPath("."),
|
||||
makeEntry(mwa.requiresPrompt(), mwa.method, mwa.arguments));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -244,7 +289,7 @@ public class TraceRecorderTarget extends AbstractTarget {
|
|||
return Map.of();
|
||||
}
|
||||
return Map.of(display, new ActionEntry(display, name, description, false,
|
||||
() -> enabled.test(object), () -> action.apply(object)));
|
||||
() -> enabled.test(object), prompt -> action.apply(object)));
|
||||
}
|
||||
|
||||
private TargetExecutionState getStateOf(TargetObject object) {
|
||||
|
@ -541,6 +586,12 @@ public class TraceRecorderTarget extends AbstractTarget {
|
|||
return spec.toggle(enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> forceTerminateAsync() {
|
||||
recorder.stopRecording();
|
||||
return AsyncUtils.nil();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> disconnectAsync() {
|
||||
return recorder.getTarget()
|
||||
|
|
|
@ -58,6 +58,10 @@ public abstract class AbstractTarget implements Target {
|
|||
this.tool = tool;
|
||||
}
|
||||
|
||||
public PluginTool getTool() {
|
||||
return tool;
|
||||
}
|
||||
|
||||
private Address staticToDynamicAddress(ProgramLocation location) {
|
||||
DebuggerStaticMappingService mappingService =
|
||||
tool.getService(DebuggerStaticMappingService.class);
|
||||
|
@ -73,10 +77,14 @@ public abstract class AbstractTarget implements Target {
|
|||
}
|
||||
|
||||
protected Address findAddress(Navigatable nav) {
|
||||
if (nav.isDynamic()) {
|
||||
return nav.getLocation().getAddress();
|
||||
ProgramLocation location = nav.getLocation();
|
||||
if (location == null) {
|
||||
return null;
|
||||
}
|
||||
return staticToDynamicAddress(nav.getLocation());
|
||||
if (nav.isDynamic()) {
|
||||
return location.getAddress();
|
||||
}
|
||||
return staticToDynamicAddress(location);
|
||||
}
|
||||
|
||||
protected Address findAddress(MarkerLocation location) {
|
||||
|
@ -118,7 +126,7 @@ public abstract class AbstractTarget implements Target {
|
|||
}
|
||||
|
||||
protected AddressRange singleRange(AddressSetView set) {
|
||||
if (set.getNumAddressRanges() != 1) {
|
||||
if (set == null || set.getNumAddressRanges() != 1) {
|
||||
return null;
|
||||
}
|
||||
return set.getFirstRange();
|
||||
|
@ -386,6 +394,11 @@ public abstract class AbstractTarget implements Target {
|
|||
runSync(msg, () -> toggleBreakpointAsync(breakpoint, enabled));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forceTerminate() {
|
||||
runSync("force terminate", () -> forceTerminateAsync());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
runSync("disconnect", this::disconnectAsync);
|
||||
|
|
|
@ -463,7 +463,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
|||
}
|
||||
|
||||
@AutoServiceConsumed
|
||||
private void setModelService(DebuggerTargetService targetService) {
|
||||
private void setTargetService(DebuggerTargetService targetService) {
|
||||
if (this.targetService != null) {
|
||||
this.targetService.removeTargetPublicationListener(forTargetsListener);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue