GP-3872: Port scripting API to Trace RMI

This commit is contained in:
Dan 2024-03-25 15:20:38 -04:00
parent 77923fa693
commit ad6cb5892d
32 changed files with 3135 additions and 2063 deletions

View File

@ -16,6 +16,7 @@
package ghidra.app.services;
import java.util.Collection;
import java.util.List;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.framework.plugintool.ServiceInfo;
@ -35,4 +36,12 @@ public interface TraceRmiLauncherService {
* @return the offers
*/
Collection<TraceRmiLaunchOffer> getOffers(Program program);
/**
* Get offers with a saved configuration, ordered by most-recently-saved
*
* @param program the program
* @return the offers
*/
List<TraceRmiLaunchOffer> getSavedOffers(Program program);
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.model;
package ghidra.debug.api.model;
import java.awt.Component;
import java.util.Collection;

View File

@ -0,0 +1,34 @@
/* ###
* 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.model;
import docking.DefaultActionContext;
import ghidra.trace.model.target.TraceObjectKeyPath;
/**
* Really just used by scripts to get a path into an action context
*/
public class DebuggerSingleObjectPathActionContext extends DefaultActionContext {
private final TraceObjectKeyPath path;
public DebuggerSingleObjectPathActionContext(TraceObjectKeyPath path) {
this.path = path;
}
public TraceObjectKeyPath getPath() {
return path;
}
}

View File

@ -169,6 +169,20 @@ public interface Target {
*/
Map<String, ActionEntry> collectActions(ActionName name, ActionContext context);
/**
* Execute a command as if in the CLI
*
* @param command the command
* @param toString true to capture the output and return it, false to print to the terminal
* @return the captured output, or null if {@code toString} is false
*/
CompletableFuture<String> executeAsync(String command, boolean toString);
/**
* @see #executeAsync(String, boolean)
*/
String execute(String command, boolean toString);
/**
* Get the trace thread that contains the given object
*

View File

@ -0,0 +1,656 @@
/* ###
* 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.flatapi;
import java.lang.invoke.MethodHandles;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import ghidra.app.services.DebuggerModelService;
import ghidra.dbg.AnnotatedDebuggerAttributeListener;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
import ghidra.dbg.target.TargetSteppable.TargetStepKind;
import ghidra.dbg.util.PathUtils;
import ghidra.debug.api.model.DebuggerProgramLaunchOffer;
import ghidra.debug.api.model.DebuggerProgramLaunchOffer.*;
import ghidra.debug.api.model.TraceRecorder;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Trace;
import ghidra.trace.model.thread.TraceThread;
import ghidra.util.Swing;
import ghidra.util.task.TaskMonitor;
@Deprecated
public interface FlatDebuggerRecorderAPI extends FlatDebuggerAPI {
/**
* Get the model (legacy target) service
*
* @return the service
*/
default DebuggerModelService getModelService() {
return requireService(DebuggerModelService.class);
}
/**
* Get the target for a given trace
*
* <p>
* WARNING: This method will likely change or be removed in the future.
*
* @param trace the trace
* @return the target, or null if not alive
*/
default TargetObject getTarget(Trace trace) {
TraceRecorder recorder = getModelService().getRecorder(trace);
if (recorder == null) {
return null;
}
return recorder.getTarget();
}
/**
* Get the target thread for a given trace thread
*
* <p>
* WARNING: This method will likely change or be removed in the future.
*
* @param thread the trace thread
* @return the target thread, or null if not alive
*/
default TargetThread getTargetThread(TraceThread thread) {
TraceRecorder recorder = getModelService().getRecorder(thread.getTrace());
if (recorder == null) {
return null;
}
return recorder.getTargetThread(thread);
}
/**
* Get the user focus for a given trace
*
* <p>
* WARNING: This method will likely change or be removed in the future.
*
* @param trace the trace
* @return the target, or null if not alive
*/
default TargetObject getTargetFocus(Trace trace) {
TraceRecorder recorder = getModelService().getRecorder(trace);
if (recorder == null) {
return null;
}
TargetObject focus = recorder.getFocus();
return focus != null ? focus : recorder.getTarget();
}
/**
* Find the most suitable object related to the given object implementing the given interface
*
* <p>
* WARNING: This method will likely change or be removed in the future.
*
* @param <T> the interface type
* @param seed the seed object
* @param iface the interface class
* @return the related interface, or null
* @throws ClassCastException if the model violated its schema wrt. the requested interface
*/
@SuppressWarnings("unchecked")
default <T extends TargetObject> T findInterface(TargetObject seed, Class<T> iface) {
DebuggerObjectModel model = seed.getModel();
List<String> found = model
.getRootSchema()
.searchForSuitable(iface, seed.getPath());
if (found == null) {
return null;
}
try {
Object value = waitOn(model.fetchModelValue(found));
return (T) value;
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
return null;
}
}
/**
* Find the most suitable object related to the given thread implementing the given interface
*
* @param <T> the interface type
* @param thread the thread
* @param iface the interface class
* @return the related interface, or null
* @throws ClassCastException if the model violated its schema wrt. the requested interface
*/
default <T extends TargetObject> T findInterface(TraceThread thread, Class<T> iface) {
TargetThread targetThread = getTargetThread(thread);
if (targetThread == null) {
return null;
}
return findInterface(targetThread, iface);
}
/**
* Find the most suitable object related to the given trace's focus implementing the given
* interface
*
* @param <T> the interface type
* @param trace the trace
* @param iface the interface class
* @return the related interface, or null
* @throws ClassCastException if the model violated its schema wrt. the requested interface
*/
default <T extends TargetObject> T findInterface(Trace trace, Class<T> iface) {
TargetObject focus = getTargetFocus(trace);
if (focus == null) {
return null;
}
return findInterface(focus, iface);
}
/**
* Find the interface related to the current thread or trace
*
* <p>
* This first attempts to find the most suitable object related to the current trace thread. If
* that fails, or if there is no current thread, it tries to find the one related to the current
* trace (or its focus). If there is no current trace, this throws an exception.
*
* @param <T> the interface type
* @param iface the interface class
* @return the related interface, or null
* @throws IllegalStateException if there is no current trace
*/
default <T extends TargetObject> T findInterface(Class<T> iface) {
TraceThread thread = getCurrentThread();
T t = thread == null ? null : findInterface(thread, iface);
if (t != null) {
return t;
}
return findInterface(requireCurrentTrace(), iface);
}
/**
* Step the given target object
*
* @param steppable the steppable target object
* @param kind the kind of step to take
* @return true if successful, false otherwise
*/
default boolean step(TargetSteppable steppable, TargetStepKind kind) {
if (steppable == null) {
return false;
}
try {
waitOn(steppable.step(kind));
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
return false;
}
return true;
}
/**
* Step the given thread on target according to the given kind
*
* @param thread the trace thread
* @param kind the kind of step to take
* @return true if successful, false otherwise
*/
default boolean step(TraceThread thread, TargetStepKind kind) {
if (thread == null) {
return false;
}
return step(findInterface(thread, TargetSteppable.class), kind);
}
/**
* Resume execution of the given target object
*
* @param resumable the resumable target object
* @return true if successful, false otherwise
*/
default boolean resume(TargetResumable resumable) {
if (resumable == null) {
return false;
}
try {
waitOn(resumable.resume());
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
return false;
}
return true;
}
/**
* Interrupt execution of the given target object
*
* @param interruptible the interruptible target object
* @return true if successful, false otherwise
*/
default boolean interrupt(TargetInterruptible interruptible) {
if (interruptible == null) {
return false;
}
try {
waitOn(interruptible.interrupt());
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
return false;
}
return true;
}
/**
* Terminate execution of the given target object
*
* @param interruptible the interruptible target object
* @return true if successful, false otherwise
*/
default boolean kill(TargetKillable killable) {
if (killable == null) {
return false;
}
try {
waitOn(killable.kill());
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
return false;
}
return true;
}
/**
* Get the current state of the given target
*
* <p>
* Any invalidated object is considered {@link TargetExecutionState#TERMINATED}. Otherwise, it's
* at least considered {@link TargetExecutionState#ALIVE}. A more specific state may be
* determined by searching the model for the conventionally-related object implementing
* {@link TargetObjectStateful}. This method applies this convention.
*
* @param target the target object
* @return the target object's execution state
*/
default TargetExecutionState getExecutionState(TargetObject target) {
if (!target.isValid()) {
return TargetExecutionState.TERMINATED;
}
TargetExecutionStateful stateful = findInterface(target, TargetExecutionStateful.class);
return stateful == null ? TargetExecutionState.ALIVE : stateful.getExecutionState();
}
/**
* Waits for the given target to exit the {@link TargetExecutionState#RUNNING} state
*
* <p>
* <b>NOTE:</b> There may be subtleties depending on the target debugger. For the most part, if
* the connection is handling a single target, things will work as expected. However, if there
* are multiple targets on one connection, it is possible for the given target to break, but for
* the target debugger to remain unresponsive to commands. This would happen, e.g., if a second
* target on the same connection is still running.
*
* @param target the target
* @param timeout the maximum amount of time to wait
* @param unit the units for time
* @throws TimeoutException if the timeout expires
*/
default void waitForBreak(TargetObject target, long timeout, TimeUnit unit)
throws TimeoutException {
TargetExecutionStateful stateful = findInterface(target, TargetExecutionStateful.class);
if (stateful == null) {
throw new IllegalArgumentException("Given target is not stateful");
}
var listener = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) {
CompletableFuture<Void> future = new CompletableFuture<>();
@AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME)
private void stateChanged(TargetObject parent, TargetExecutionState state) {
if (parent == stateful && !state.isRunning()) {
future.complete(null);
}
}
};
target.getModel().addModelListener(listener);
try {
if (!stateful.getExecutionState().isRunning()) {
return;
}
listener.future.get(timeout, unit);
}
catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
finally {
target.getModel().removeModelListener(listener);
}
}
@Override
default void waitForBreak(Trace trace, long timeout, TimeUnit unit) throws TimeoutException {
TargetObject target = getTarget(trace);
if (target == null || !target.isValid()) {
return;
}
waitForBreak(target, timeout, unit);
}
/**
* Execute a command in a connection's interpreter, capturing the output
*
* <p>
* This executes a raw command in the given interpreter. The command could have arbitrary
* effects, so it may be necessary to wait for those effects to be handled by the tool's
* services and plugins before proceeding.
*
* @param interpreter the interpreter
* @param command the command
* @return the output, or null if there is no interpreter
*/
default String executeCapture(TargetInterpreter interpreter, String command) {
if (interpreter == null) {
return null;
}
try {
return waitOn(interpreter.executeCapture(command));
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
return null;
}
}
/**
* Execute a command in a connection's interpreter
*
* <p>
* This executes a raw command in the given interpreter. The command could have arbitrary
* effects, so it may be necessary to wait for those effects to be handled by the tool's
* services and plugins before proceeding.
*
* @param interpreter the interpreter
* @param command the command
* @return true if successful
*/
default boolean execute(TargetInterpreter interpreter, String command) {
if (interpreter == null) {
return false;
}
try {
waitOn(interpreter.executeCapture(command));
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
return false;
}
return true;
}
/**
* Get the value at the given path for the given model
*
* @param model the model
* @param path the path
* @return the avlue, or null if the trace is not live or if the path does not exist
*/
default Object getModelValue(DebuggerObjectModel model, String path) {
try {
return waitOn(model.fetchModelValue(PathUtils.parse(path)));
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
return null;
}
}
/**
* Get the value at the given path for the current trace's model
*
* @param path the path
* @return the value, or null if the trace is not live or if the path does not exist
*/
default Object getModelValue(String path) {
TraceRecorder recorder = getModelService().getRecorder(getCurrentTrace());
if (recorder == null) {
return null;
}
return getModelValue(recorder.getTarget().getModel(), path);
}
/**
* Refresh the given objects children (elements and attributes)
*
* @param object the object
* @return the set of children, excluding primitive-valued attributes
*/
default Set<TargetObject> refreshObjectChildren(TargetObject object) {
try {
// Refresh both children and memory/register values
waitOn(object.invalidateCaches());
waitOn(object.resync());
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
return null;
}
Set<TargetObject> result = new LinkedHashSet<>();
result.addAll(object.getCachedElements().values());
for (Object v : object.getCachedAttributes().values()) {
if (v instanceof TargetObject) {
result.add((TargetObject) v);
}
}
return result;
}
/**
* Refresh the given object and its children, recursively
*
* <p>
* The objects are traversed in depth-first pre-order. Links are traversed, even if the object
* is not part of the specified subtree, but an object is skipped if it has already been
* visited.
*
* @param object the seed object
* @return true if the traversal completed successfully
*/
default boolean refreshSubtree(TargetObject object) {
var util = new Object() {
Set<TargetObject> visited = new HashSet<>();
boolean visit(TargetObject object) {
if (!visited.add(object)) {
return true;
}
for (TargetObject child : refreshObjectChildren(object)) {
if (!visit(child)) {
return false;
}
}
return true;
}
};
return util.visit(object);
}
/**
* {@inheritDoc}
*
* <p>
* This override includes flushing the recorder's event and transaction queues.
*/
@Override
default boolean flushAsyncPipelines(Trace trace) {
try {
TraceRecorder recorder = getModelService().getRecorder(trace);
if (recorder != null) {
waitOn(recorder.getTarget().getModel().flushEvents());
waitOn(recorder.flushTransactions());
}
trace.flushEvents();
waitOn(getMappingService().changesSettled());
waitOn(getBreakpointService().changesSettled());
Swing.allowSwingToProcessEvents();
return true;
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
return false;
}
}
/**
* Get offers for launching the given program
*
* @param program the program
* @return the offers
*/
default List<DebuggerProgramLaunchOffer> getLaunchOffers(Program program) {
return getModelService().getProgramLaunchOffers(program).collect(Collectors.toList());
}
/**
* Get offers for launching the current program
*
* @return the offers
*/
default List<DebuggerProgramLaunchOffer> getLaunchOffers() {
return getLaunchOffers(requireCurrentProgram());
}
/**
* Get the best launch offer for a program, throwing an exception if there is no offer
*
* @param program the program
* @return the offer
* @throws NoSuchElementException if there is no offer
*/
default DebuggerProgramLaunchOffer requireLaunchOffer(Program program) {
Optional<DebuggerProgramLaunchOffer> offer =
getModelService().getProgramLaunchOffers(program).findFirst();
if (offer.isEmpty()) {
throw new NoSuchElementException("No offers to launch " + program);
}
return offer.get();
}
/**
* Launch the given offer, overriding its command line
*
* <p>
* <b>NOTE:</b> Most offers take a command line, but not all do. If this is used for an offer
* that does not, it's behavior is undefined.
*
* <p>
* Launches are not always successful, and may in fact fail frequently, usually because of
* configuration errors or missing components on the target platform. This may leave stale
* connections and/or target debuggers, processes, etc., in strange states. Furthermore, even if
* launching the target is successful, starting the recorder may not succeed, typically because
* Ghidra cannot identify and map the target platform to a Sleigh language. This method makes no
* attempt at cleaning up partial pieces. Instead it returns those pieces in the launch result.
* If the result includes a recorder, the launch was successful. If not, the script can decide
* what to do with the other pieces. That choice depends on what is expected of the user. Can
* the user reasonable be expected to intervene and complete the launch manually? How many
* targets does the script intend to launch? How big is the mess if left partially completed?
*
* @param offer the offer (this includes the program given when asking for offers)
* @param commandLine the command-line override. If this doesn't refer to the same program as
* the offer, there may be unexpected results
* @param monitor the monitor for the launch stages
* @return the result, possibly partial
*/
default LaunchResult launch(DebuggerProgramLaunchOffer offer, String commandLine,
TaskMonitor monitor) {
try {
return waitOn(offer.launchProgram(monitor, PromptMode.NEVER, new LaunchConfigurator() {
@Override
public Map<String, ?> configureLauncher(TargetLauncher launcher,
Map<String, ?> arguments, RelPrompt relPrompt) {
Map<String, Object> adjusted = new HashMap<>(arguments);
adjusted.put(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, commandLine);
return adjusted;
}
}));
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
// TODO: This is not ideal, since it's likely partially completed
return LaunchResult.totalFailure(e);
}
}
/**
* Launch the given offer with the default/saved arguments
*
* @see #launch(DebuggerProgramLaunchOffer, String, TaskMonitor)
*/
default LaunchResult launch(DebuggerProgramLaunchOffer offer, TaskMonitor monitor) {
try {
return waitOn(offer.launchProgram(monitor, PromptMode.NEVER));
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
// TODO: This is not ideal, since it's likely partially completed
return LaunchResult.totalFailure(e);
}
}
/**
* Launch the given program, overriding its command line
*
* <p>
* This takes the best offer for the given program. The command line should invoke the given
* program. If it does not, there may be unexpected results.
*
* @see #launch(DebuggerProgramLaunchOffer, String, TaskMonitor)
*/
default LaunchResult launch(Program program, String commandLine, TaskMonitor monitor)
throws InterruptedException, ExecutionException, TimeoutException {
return launch(requireLaunchOffer(program), commandLine, monitor);
}
/**
* Launch the given program with the default/saved arguments
*
* <p>
* This takes the best offer for the given program.
*
* @see #launch(DebuggerProgramLaunchOffer, String, TaskMonitor)
*/
default LaunchResult launch(Program program, TaskMonitor monitor)
throws InterruptedException, ExecutionException, TimeoutException {
return launch(requireLaunchOffer(program), monitor);
}
/**
* Launch the current program, overriding its command line
*
* @see #launch(Program, String, TaskMonitor)
*/
default LaunchResult launch(String commandLine, TaskMonitor monitor)
throws InterruptedException, ExecutionException, TimeoutException {
return launch(requireCurrentProgram(), commandLine, monitor);
}
/**
* Launch the current program with the default/saved arguments
*
* @see #launch(Program, TaskMonitor)
*/
default LaunchResult launch(TaskMonitor monitor)
throws InterruptedException, ExecutionException, TimeoutException {
return launch(requireCurrentProgram(), monitor);
}
}

View File

@ -0,0 +1,162 @@
/* ###
* 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.flatapi;
import java.util.*;
import ghidra.app.services.TraceRmiLauncherService;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*;
import ghidra.program.model.listing.Program;
import ghidra.util.task.TaskMonitor;
public interface FlatDebuggerRmiAPI extends FlatDebuggerAPI {
/**
* Get the trace-rmi launcher service
*
* @return the service
*/
default TraceRmiLauncherService getTraceRmiLauncherService() {
return requireService(TraceRmiLauncherService.class);
}
/**
* Get offers for launching the given program
*
* @param program the program, or null for no image
* @return the offers
*/
default Collection<TraceRmiLaunchOffer> getLaunchOffers(Program program) {
return getTraceRmiLauncherService().getOffers(program);
}
/**
* Get offers for launching the current program
*
* @return the offers
*/
default Collection<TraceRmiLaunchOffer> getLaunchOffers() {
return getLaunchOffers(getCurrentProgram());
}
/**
* Get saved offers for launching the given program, ordered by most-recently-saved
*
* @param program the program, or null for no image
* @return the offers
*/
default List<TraceRmiLaunchOffer> getSavedLaunchOffers(Program program) {
return getTraceRmiLauncherService().getSavedOffers(program);
}
/**
* Get saved offers for launching the current program, ordered by most-recently-saved
*
* @return the offers
*/
default List<TraceRmiLaunchOffer> getSavedLaunchOffers() {
return getSavedLaunchOffers(getCurrentProgram());
}
/**
* Get the most-recently-saved launch offer for the given program
*
* @param program the program, or null for no image
* @return the offer
* @throws NoSuchElementException if no offer's configuration has been saved
*/
default TraceRmiLaunchOffer requireLastLaunchOffer(Program program) {
List<TraceRmiLaunchOffer> offers = getSavedLaunchOffers(program);
if (offers.isEmpty()) {
throw new NoSuchElementException("No saved offers to launch " + program);
}
return offers.get(0);
}
/**
* Get the most-recently-saved launch offer for the current program
*
* @return the offer
* @throws NoSuchElementException if no offer's configuration has been saved
*/
default TraceRmiLaunchOffer requireLastLaunchOffer() {
return requireLastLaunchOffer(getCurrentProgram());
}
/**
* Launch the given offer with the default, saved, and/or overridden arguments
*
* <p>
* If the offer has saved arguments, those will be loaded. Otherwise, the default arguments will
* be used. If given, specific arguments can be overridden by the caller. The caller may need to
* examine the offer's parameters before overriding any arguments. Conventionally, the argument
* displayed as "Image" gives the path to the executable, and "Args" gives the command-line
* arguments to pass to the target.
*
* @param offer the offer to launch
* @param monitor a monitor for the launch stages
* @param overrideArgs overridden arguments, which may be empty
* @return the launch result, which may indicate errors
*/
default LaunchResult launch(TraceRmiLaunchOffer offer, Map<String, ?> overrideArgs,
TaskMonitor monitor) {
return offer.launchProgram(monitor, new LaunchConfigurator() {
@Override
public Map<String, ?> configureLauncher(TraceRmiLaunchOffer offer,
Map<String, ?> arguments, RelPrompt relPrompt) {
if (arguments.isEmpty()) {
return arguments;
}
Map<String, Object> args = new HashMap<>(arguments);
args.putAll(overrideArgs);
return args;
}
});
}
/**
* Launch the given offer with the default or saved arguments
*
* @param offer the offer to launch
* @param monitor a monitor for the launch stages
* @return the launch result, which may indicate errors
*/
default LaunchResult launch(TraceRmiLaunchOffer offer, TaskMonitor monitor) {
return launch(offer, Map.of(), monitor);
}
/**
* Launch the given program with the most-recently-saved offer
*
* @param program the program to launch
* @param monitor a monitor for the launch stages
* @return the launch result, which may indicate errors
*/
default LaunchResult launch(Program program, TaskMonitor monitor) {
return launch(requireLastLaunchOffer(program), monitor);
}
/**
* Launch the current program with the most-recently-saved offer
*
* @param monitor a monitor for the launch stages
* @return the launch result, which may indicate errors
*/
default LaunchResult launch(TaskMonitor monitor) {
return launch(requireLastLaunchOffer(), monitor);
}
}

View File

@ -146,6 +146,15 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
this.terminalService = Objects.requireNonNull(tool.getService(TerminalService.class));
}
@Override
public boolean equals(Object obj) {
if (this.getClass() != obj.getClass()) {
return false;
}
AbstractTraceRmiLaunchOffer other = (AbstractTraceRmiLaunchOffer) obj;
return this.getConfigName().equals(other.getConfigName());
}
protected int getTimeoutMillis() {
return DEFAULT_TIMEOUT_MILLIS;
}

View File

@ -192,6 +192,15 @@ public class TraceRmiLauncherServicePlugin extends Plugin
.toList();
}
@Override
public List<TraceRmiLaunchOffer> getSavedOffers(Program program) {
Map<String, Long> savedConfigs = loadSavedConfigs(program);
return getOffers(program).stream()
.filter(o -> savedConfigs.containsKey(o.getConfigName()))
.sorted(Comparator.comparing(o -> -savedConfigs.get(o.getConfigName())))
.toList();
}
protected void executeTask(Task task) {
ProgressService progressService = tool.getService(ProgressService.class);
if (progressService != null) {

View File

@ -25,7 +25,6 @@ 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;
@ -38,6 +37,7 @@ import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.dbg.util.PathMatcher;
import ghidra.dbg.util.PathPredicates;
import ghidra.dbg.util.PathPredicates.Align;
import ghidra.debug.api.model.DebuggerObjectActionContext;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.debug.api.tracermi.*;
@ -51,6 +51,7 @@ 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.TraceMemoryRegion;
import ghidra.trace.model.memory.TraceObjectMemoryRegion;
import ghidra.trace.model.stack.*;
import ghidra.trace.model.target.*;
@ -611,6 +612,13 @@ public class TraceRmiTarget extends AbstractTarget {
}
}
record ExecuteMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
static final ExecuteMatcher HAS_CMD_TOSTRING = new ExecuteMatcher(2, List.of(
new TypeParamSpec("command", String.class),
new TypeParamSpec("toString", Boolean.class)));
static final List<ExecuteMatcher> ALL = matchers(HAS_CMD_TOSTRING);
}
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),
@ -841,6 +849,18 @@ public class TraceRmiTarget extends AbstractTarget {
}
}
@Override
public CompletableFuture<String> executeAsync(String command, boolean toString) {
MatchedMethod execute = matches.getBest("execute", ActionName.EXECUTE, ExecuteMatcher.ALL);
if (execute == null) {
return CompletableFuture.failedFuture(new NoSuchElementException());
}
Map<String, Object> args = new HashMap<>();
args.put(execute.params.get("command").name(), command);
args.put(execute.params.get("toString").name(), toString);
return execute.method.invokeAsync(args).toCompletableFuture().thenApply(v -> (String) v);
}
@Override
public CompletableFuture<Void> activateAsync(DebuggerCoordinates prev,
DebuggerCoordinates coords) {
@ -906,15 +926,11 @@ public class TraceRmiTarget extends AbstractTarget {
}
protected TraceObject getProcessForSpace(AddressSpace space) {
for (TraceObjectValue objVal : trace.getObjectManager()
.getValuesIntersecting(
for (TraceMemoryRegion region : trace.getMemoryManager()
.getRegionsIntersecting(
Lifespan.at(getSnap()),
new AddressRangeImpl(space.getMinAddress(), space.getMaxAddress()),
TargetMemoryRegion.RANGE_ATTRIBUTE_NAME)) {
TraceObject obj = objVal.getParent();
if (!obj.getInterfaces().contains(TraceObjectMemoryRegion.class)) {
continue;
}
new AddressRangeImpl(space.getMinAddress(), space.getMaxAddress()))) {
TraceObject obj = ((TraceObjectMemoryRegion) region).getObject();
return obj.queryCanonicalAncestorsTargetInterface(TargetProcess.class)
.findFirst()
.orElse(null);

View File

@ -20,12 +20,14 @@ import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Function;
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;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.target.Target;
@ -65,6 +67,19 @@ public class TestTraceRmiConnection implements TraceRmiConnection {
retType);
}
public TestRemoteMethod(String name, ActionName action, String display, String description,
Map<String, RemoteParameter> parameters, TargetObjectSchema retType) {
this(name, action, display, description, parameters, retType.getName(),
new AsyncPairingQueue<>(), new AsyncPairingQueue<>());
}
public TestRemoteMethod(String name, ActionName action, String display, String description,
TargetObjectSchema retType, RemoteParameter... parameters) {
this(name, action, display, description, Stream.of(parameters)
.collect(Collectors.toMap(RemoteParameter::name, p -> p)),
retType);
}
@Override
public RemoteAsyncResult invokeAsync(Map<String, Object> arguments) {
argQueue.give().complete(arguments);
@ -80,10 +95,24 @@ public class TestTraceRmiConnection implements TraceRmiConnection {
public void result(Object ret) {
retQueue.give().complete(ret);
}
public CompletableFuture<Map<String, Object>> expect(
Function<Map<String, Object>, Object> impl) {
record ArgsRet(Map<String, Object> args, Object ret) {
}
var result = argQueue().take().thenApply(a -> new ArgsRet(a, impl.apply(a)));
result.thenApply(ar -> ar.ret).handle(AsyncUtils.copyTo(retQueue().give()));
return result.thenApply(ar -> ar.args);
}
}
public record TestRemoteParameter(String name, SchemaName type, boolean required,
Object defaultValue, String display, String description) implements RemoteParameter {
public TestRemoteParameter(String name, TargetObjectSchema type, boolean required,
Object defaultValue, String display, String description) {
this(name, type.getName(), required, defaultValue, display, description);
}
@Override
public Object getDefaultValue() {
return defaultValue;

View File

@ -27,12 +27,12 @@ import java.util.concurrent.TimeUnit;
import ghidra.app.script.GhidraScript;
import ghidra.debug.api.breakpoint.LogicalBreakpoint;
import ghidra.debug.api.model.DebuggerProgramLaunchOffer.LaunchResult;
import ghidra.debug.flatapi.FlatDebuggerAPI;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.LaunchResult;
import ghidra.debug.flatapi.FlatDebuggerRmiAPI;
import ghidra.program.model.address.Address;
import ghidra.trace.model.Trace;
public class DemoDebuggerScript extends GhidraScript implements FlatDebuggerAPI {
public class DemoDebuggerScript extends GhidraScript implements FlatDebuggerRmiAPI {
@Override
protected void run() throws Exception {
@ -55,25 +55,19 @@ public class DemoDebuggerScript extends GhidraScript implements FlatDebuggerAPI
if (result.exception() != null) {
printerr("Failed to launch " + currentProgram + ": " + result.exception());
if (result.model() != null) {
result.model().close();
}
if (result.recorder() != null) {
closeTrace(result.recorder().getTrace());
}
result.close();
return;
}
Trace trace = result.recorder().getTrace();
Trace trace = result.trace();
println("Successfully launched in trace " + trace);
/**
* Breakpoints are highly dependent on the module map. To work correctly: 1) The target
* debugger must provide the module map. 2) Ghidra must have recorded that module map into
* the trace. 3) Ghidra must recognize the module names and map them to programs open in the
* tool. These events all occur asynchronously, usually immediately after launch. Most
* launchers will wait for the target program module to be mapped to its Ghidra program
* database, but the breakpoint service may still be processing the new mapping.
* Breakpoints are highly dependent on the module map. To work correctly, the target
* debugger must record the module map into the trace, and Ghidra must recognize the module
* names and map them to programs open in the tool. These events all occur asynchronously,
* usually immediately after launch. Most launchers will wait for the target program module
* to be mapped to its Ghidra program database, but the breakpoint service may still be
* processing the new mapping.
*/
flushAsyncPipelines(trace);
@ -104,8 +98,8 @@ public class DemoDebuggerScript extends GhidraScript implements FlatDebuggerAPI
while (isTargetAlive()) {
waitForBreak(10, TimeUnit.SECONDS);
/**
* The recorder is going to schedule some reads upon break, so let's allow them to
* settle.
* The target is going to perform some reads upon break, so let's allow them to
* complete.
*/
flushAsyncPipelines(trace);

View File

@ -20,9 +20,9 @@ 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;
import ghidra.debug.flatapi.FlatDebuggerRecorderAPI;;
public class MonitorModelEventsScript extends GhidraScript implements FlatDebuggerAPI {
public class MonitorModelEventsScript extends GhidraScript implements FlatDebuggerRecorderAPI {
static DebuggerModelListener listener = new DebuggerModelListener() {
@Override
public void attributesChanged(TargetObject object, Collection<String> removed,

View File

@ -30,7 +30,6 @@ 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.model.DebuggerObjectActionContext;
import ghidra.app.services.*;
import ghidra.app.services.DebuggerControlService.ControlModeChangeListener;
import ghidra.app.services.DebuggerEmulationService.CachedEmulator;
@ -39,6 +38,7 @@ import ghidra.app.services.DebuggerTraceManagerService.ActivationCause;
import ghidra.async.AsyncUtils;
import ghidra.debug.api.control.ControlMode;
import ghidra.debug.api.emulation.DebuggerPcodeMachine;
import ghidra.debug.api.model.DebuggerObjectActionContext;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.target.Target.ActionEntry;

View File

@ -29,9 +29,9 @@ import ghidra.app.context.ProgramLocationActionContext;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext;
import ghidra.app.services.*;
import ghidra.debug.api.control.ControlMode;
import ghidra.debug.api.model.DebuggerObjectActionContext;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.target.Target.ActionEntry;

View File

@ -25,6 +25,7 @@ import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
import ghidra.app.plugin.core.debug.gui.model.columns.*;
import ghidra.dbg.target.*;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.debug.api.model.DebuggerObjectActionContext;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.ServiceProvider;

View File

@ -35,10 +35,10 @@ import ghidra.app.plugin.core.debug.gui.DebuggerBlockChooserDialog;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.AbstractSelectAddressesAction;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.SelectRowsAction;
import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider;
import ghidra.app.plugin.core.debug.service.modules.MapRegionsBackgroundCommand;
import ghidra.app.services.*;
import ghidra.debug.api.model.DebuggerObjectActionContext;
import ghidra.debug.api.modules.MapProposal;
import ghidra.debug.api.modules.RegionMapProposal;
import ghidra.debug.api.modules.RegionMapProposal.RegionMapEntry;

View File

@ -29,6 +29,7 @@ import ghidra.app.plugin.core.debug.gui.model.AbstractQueryTablePanel.CellActiva
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueProperty;
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
import ghidra.app.services.DebuggerListingService;
import ghidra.debug.api.model.DebuggerObjectActionContext;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.Plugin;

View File

@ -51,6 +51,7 @@ import ghidra.app.plugin.core.debug.gui.model.ObjectTreeModel.*;
import ghidra.app.plugin.core.debug.gui.model.ObjectTreeModel.RootNode;
import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.debug.api.model.DebuggerObjectActionContext;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.target.Target.ActionEntry;

View File

@ -28,6 +28,7 @@ import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingUtils;
import ghidra.dbg.target.TargetModule;
import ghidra.dbg.target.TargetProcess;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.debug.api.model.DebuggerObjectActionContext;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.ServiceProvider;

View File

@ -41,11 +41,11 @@ 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.*;
import ghidra.debug.api.model.DebuggerObjectActionContext;
import ghidra.debug.api.modules.*;
import ghidra.debug.api.modules.ModuleMapProposal.ModuleMapEntry;
import ghidra.debug.api.modules.SectionMapProposal.SectionMapEntry;

View File

@ -23,7 +23,6 @@ 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;
@ -39,7 +38,7 @@ import ghidra.dbg.target.TargetMethod.TargetParameterMap;
import ghidra.dbg.target.TargetSteppable.TargetStepKind;
import ghidra.dbg.util.PathMatcher;
import ghidra.dbg.util.PathPredicates;
import ghidra.debug.api.model.TraceRecorder;
import ghidra.debug.api.model.*;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginTool;
@ -113,6 +112,13 @@ public class TraceRecorderTarget extends AbstractTarget {
}
return iface.cast(recorder.getTargetObject(suitable));
}
else if (context instanceof DebuggerSingleObjectPathActionContext ctx) {
TargetObject targetObject = recorder.getTargetObject(ctx.getPath());
if (targetObject == null) {
return null;
}
return targetObject.getCachedSuitable(iface);
}
return null;
}
@ -469,6 +475,18 @@ public class TraceRecorderTarget extends AbstractTarget {
return CompletableFuture.allOf(requests);
}
@Override
public CompletableFuture<String> executeAsync(String command, boolean toString) {
TargetInterpreter interpreter = findObjectInRecorder(null, TargetInterpreter.class);
if (interpreter == null) {
return AsyncUtils.nil();
}
if (toString) {
return interpreter.executeCapture(command);
}
return interpreter.execute(command).thenApply(r -> null);
}
@Override
public CompletableFuture<Void> readMemoryAsync(AddressSetView set, TaskMonitor monitor) {
return recorder.readMemoryBlocks(set, monitor)

View File

@ -307,6 +307,11 @@ public abstract class AbstractTarget implements Target {
getSyncMonitored(monitor, name, supplier);
}
@Override
public String execute(String command, boolean toString) {
return getSync("execute", () -> executeAsync(command, toString));
}
@Override
public void activate(DebuggerCoordinates prev, DebuggerCoordinates coords) {
runSync("activate", () -> activateAsync(prev, coords));

View File

@ -24,7 +24,6 @@ import ghidra.async.AsyncUtils;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.target.Target.ActionEntry;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.Register;
@ -115,6 +114,16 @@ class MockTarget implements Target {
public void invalidateMemoryCaches() {
}
@Override
public CompletableFuture<String> executeAsync(String command, boolean toString) {
return AsyncUtils.nil();
}
@Override
public String execute(String command, boolean toString) {
return null;
}
@Override
public CompletableFuture<Void> readMemoryAsync(AddressSetView set, TaskMonitor monitor) {
return AsyncUtils.nil();

View File

@ -127,6 +127,8 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
protected TestTraceRmiConnection rmiCx;
protected TestRemoteMethod rmiMethodExecute;
protected TestRemoteMethod rmiMethodResume;
protected TestRemoteMethod rmiMethodInterrupt;
protected TestRemoteMethod rmiMethodKill;
@ -145,41 +147,53 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
protected TestRemoteMethod rmiMethodReadRegs;
protected TestRemoteMethod rmiMethodWriteReg;
protected TestRemoteMethod rmiMethodReadMem;
protected TestRemoteMethod rmiMethodWriteMem;
protected void createRmiConnection() {
rmiCx = new TestTraceRmiConnection();
}
protected void addExecuteMethod() {
rmiMethodExecute = new TestRemoteMethod("execute", ActionName.EXECUTE, "Execute",
"Execut a CLI command", EnumerableTargetObjectSchema.STRING,
new TestRemoteParameter("cmd", EnumerableTargetObjectSchema.STRING, true, null,
"Command", "The command to execute"),
new TestRemoteParameter("to_string", EnumerableTargetObjectSchema.BOOL, true, false,
"To String", "Capture output to string"));
rmiCx.getMethods().add(rmiMethodExecute);
}
protected void addControlMethods() {
rmiMethodResume = new TestRemoteMethod("resume", ActionName.RESUME, "Resume",
"Resume the target", EnumerableTargetObjectSchema.VOID.getName(),
"Resume the target", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process to resume"));
rmiMethodInterrupt = new TestRemoteMethod("interrupt", ActionName.INTERRUPT, "Interrupt",
"Interrupt the target", EnumerableTargetObjectSchema.VOID.getName(),
"Interrupt the target", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process to interrupt"));
rmiMethodKill = new TestRemoteMethod("kill", ActionName.KILL, "Kill",
"Kill the target", EnumerableTargetObjectSchema.VOID.getName(),
"Kill the target", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process to kill"));
rmiMethodStepInto = new TestRemoteMethod("step_into", ActionName.STEP_INTO, "Step Into",
"Step the thread, descending into subroutines",
EnumerableTargetObjectSchema.VOID.getName(),
"Step the thread, descending into subroutines", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("thread", new SchemaName("Thread"), true, null, "Thread",
"The thread to step"));
rmiMethodStepOver = new TestRemoteMethod("step_over", ActionName.STEP_OVER, "Step Over",
"Step the thread, without descending into subroutines",
EnumerableTargetObjectSchema.VOID.getName(),
EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("thread", new SchemaName("Thread"), true, null, "Thread",
"The thread to step"));
rmiMethodStepOut = new TestRemoteMethod("step_out", ActionName.STEP_OUT, "Step Out",
"Allow the thread to finish the current subroutine",
EnumerableTargetObjectSchema.VOID.getName(),
"Allow the thread to finish the current subroutine", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("thread", new SchemaName("Thread"), true, null, "Thread",
"The thread to step"));
@ -194,56 +208,51 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
protected void addBreakpointMethods() {
rmiMethodSetHwBreak = new TestRemoteMethod("set_hw_break", ActionName.BREAK_HW_EXECUTE,
"Hardware Breakpoint",
"Place a hardware execution breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
"Hardware Breakpoint", "Place a hardware execution breakpoint",
EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process in which to place the breakpoint"),
new TestRemoteParameter("address", EnumerableTargetObjectSchema.ADDRESS.getName(), true,
new TestRemoteParameter("address", EnumerableTargetObjectSchema.ADDRESS, true,
null, "Address", "The desired address"));
rmiMethodSetSwBreak = new TestRemoteMethod("set_sw_break", ActionName.BREAK_SW_EXECUTE,
"Software Breakpoint",
"Place a software execution breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
"Software Breakpoint", "Place a software execution breakpoint",
EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process in which to place the breakpoint"),
new TestRemoteParameter("address", EnumerableTargetObjectSchema.ADDRESS.getName(), true,
new TestRemoteParameter("address", EnumerableTargetObjectSchema.ADDRESS, true,
null, "Address", "The desired address"));
rmiMethodSetReadBreak = new TestRemoteMethod("set_read_break", ActionName.BREAK_READ,
"Read Breakpoint",
"Place a read breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
"Read Breakpoint", "Place a read breakpoint", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process in which to place the breakpoint"),
new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE.getName(), true,
new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE, true,
null, "Range", "The desired address range"));
rmiMethodSetWriteBreak = new TestRemoteMethod("set_write_break", ActionName.BREAK_WRITE,
"Write Breakpoint",
"Place a write breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
"Write Breakpoint", "Place a write breakpoint", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process in which to place the breakpoint"),
new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE.getName(), true,
new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE, true,
null, "Range", "The desired address range"));
rmiMethodSetAccessBreak = new TestRemoteMethod("set_acc_break", ActionName.BREAK_ACCESS,
"Access Breakpoint",
"Place an access breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
"Access Breakpoint", "Place an access breakpoint", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process",
"The process in which to place the breakpoint"),
new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE.getName(), true,
new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE, true,
null, "Range", "The desired address range"));
rmiMethodToggleBreak = new TestRemoteMethod("toggle_break", ActionName.TOGGLE,
"Toggle Breakpoint",
"Toggle a breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
"Toggle Breakpoint", "Toggle a breakpoint", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("breakpoint", new SchemaName("BreakpointSpec"), true, null,
"Breakpoint", "The breakpoint to toggle"),
new TestRemoteParameter("enabled", EnumerableTargetObjectSchema.BOOL.getName(), true,
new TestRemoteParameter("enabled", EnumerableTargetObjectSchema.BOOL, true,
null, "Enable", "True to enable. False to disable"));
rmiMethodDeleteBreak = new TestRemoteMethod("delete_break", ActionName.DELETE,
"Delete Breakpoint",
"Delete a breakpoint", EnumerableTargetObjectSchema.VOID.getName(),
"Delete Breakpoint", "Delete a breakpoint", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("breakpoint", new SchemaName("BreakpointSpec"), true, null,
"Breakpoint", "The breakpoint to delete"));
@ -259,17 +268,17 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
protected void addRegisterMethods() {
rmiMethodReadRegs = new TestRemoteMethod("read_regs", ActionName.REFRESH, "Read Registers",
"Read registers", EnumerableTargetObjectSchema.VOID.getName(),
"Read registers", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("container", new SchemaName("RegisterContainer"), true, null,
"Registers", "The registers node to read"));
rmiMethodWriteReg = new TestRemoteMethod("write_reg", ActionName.WRITE_REG,
"Write Register", "Write a register", EnumerableTargetObjectSchema.VOID.getName(),
"Write Register", "Write a register", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("frame", new SchemaName("Frame"), false, 0, "Frame",
"The frame to write to"),
new TestRemoteParameter("name", EnumerableTargetObjectSchema.STRING.getName(), true,
new TestRemoteParameter("name", EnumerableTargetObjectSchema.STRING, true,
null, "Register", "The name of the register to write"),
new TestRemoteParameter("value", EnumerableTargetObjectSchema.BYTE_ARR.getName(), true,
new TestRemoteParameter("value", EnumerableTargetObjectSchema.BYTE_ARR, true,
null, "Value", "The desired value"));
TestRemoteMethodRegistry reg = rmiCx.getMethods();
@ -277,6 +286,28 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest
reg.add(rmiMethodWriteReg);
}
protected void addMemoryMethods() {
rmiMethodReadMem = new TestRemoteMethod("read_mem", ActionName.READ_MEM, "Read Memory",
"Read memory", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("process", new SchemaName("Process"), true, null,
"Process", "The process whose memory to read"),
new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE, true, null,
"Range", "The address range to read"));
rmiMethodWriteMem = new TestRemoteMethod("write_mem", ActionName.WRITE_MEM, "Write Memory",
"Write memory", EnumerableTargetObjectSchema.VOID,
new TestRemoteParameter("process", new SchemaName("Process"), true, null,
"Process", "The process whose memory to read"),
new TestRemoteParameter("start", EnumerableTargetObjectSchema.ADDRESS, true, null,
"Start", "The address to start writing"),
new TestRemoteParameter("data", EnumerableTargetObjectSchema.BYTE_ARR, true, null,
"Data", "The data to write"));
TestRemoteMethodRegistry reg = rmiCx.getMethods();
reg.add(rmiMethodReadMem);
reg.add(rmiMethodWriteMem);
}
protected TraceObject addMemoryRegion(TraceObjectManager objs, Lifespan lifespan,
AddressRange range, String name, String flags) {
String pathStr =

View File

@ -0,0 +1,94 @@
/* ###
* 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.launcher;
import static org.junit.Assert.assertEquals;
import java.net.SocketAddress;
import java.util.*;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.debug.api.tracermi.TerminalSession;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion;
import ghidra.program.model.listing.Program;
import ghidra.util.task.TaskMonitor;
public class TestTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion {
public static class TestTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOffer {
private static final ParameterDescription<String> PARAM_DESC_IMAGE =
ParameterDescription.create(String.class, "image", true, "",
PARAM_DISPLAY_IMAGE, "Image to execute");
public TestTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program) {
super(plugin, program);
}
public Program getProgram() {
return program;
}
@Override
public String getConfigName() {
return "TEST";
}
@Override
public String getTitle() {
return "Test";
}
@Override
public String getDescription() {
return "Test launch offer";
}
@Override
public Map<String, ParameterDescription<?>> getParameters() {
return Map.ofEntries(Map.entry(PARAM_DESC_IMAGE.name, PARAM_DESC_IMAGE));
}
@Override
public boolean requiresImage() {
return false;
}
@Override
protected void launchBackEnd(TaskMonitor monitor, Map<String, TerminalSession> sessions,
Map<String, ?> args, SocketAddress address) throws Exception {
}
@Override
public LaunchResult launchProgram(TaskMonitor monitor, LaunchConfigurator configurator) {
assertEquals(PromptMode.NEVER, configurator.getPromptMode());
Map<String, ?> args =
configurator.configureLauncher(this, loadLastLauncherArgs(false), RelPrompt.NONE);
return new LaunchResult(program, null, null, null, null,
new RuntimeException("Test launcher cannot launch " + args.get("image")));
}
public void saveLauncherArgs(Map<String, ?> args) {
super.saveLauncherArgs(args, getParameters());
}
}
@Override
public Collection<TraceRmiLaunchOffer> getOffers(TraceRmiLauncherServicePlugin plugin,
Program program) {
return List.of(new TestTraceRmiLaunchOffer(plugin, program));
}
}

View File

@ -0,0 +1,293 @@
/* ###
* 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.flatapi;
import static org.junit.Assert.*;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import org.junit.Before;
import org.junit.Test;
import db.Transaction;
import ghidra.app.plugin.assembler.Assembler;
import ghidra.app.plugin.assembler.Assemblers;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerIntegrationTest;
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin;
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingProvider;
import ghidra.app.plugin.core.debug.service.breakpoint.DebuggerLogicalBreakpointServicePlugin;
import ghidra.app.plugin.core.debug.service.control.DebuggerControlServicePlugin;
import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePlugin;
import ghidra.app.services.*;
import ghidra.debug.api.control.ControlMode;
import ghidra.program.model.address.Address;
import ghidra.program.model.lang.*;
import ghidra.trace.database.memory.DBTraceMemoryManager;
import ghidra.trace.database.memory.DBTraceMemorySpace;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.breakpoint.TraceBreakpointKind;
import ghidra.trace.model.memory.TraceMemoryFlag;
import ghidra.trace.model.stack.TraceStack;
import ghidra.trace.model.thread.TraceThread;
public abstract class AbstractFlatDebuggerAPITest<API extends FlatDebuggerAPI>
extends AbstractGhidraHeadedDebuggerIntegrationTest {
protected DebuggerLogicalBreakpointService breakpointService;
protected DebuggerStaticMappingService mappingService;
protected DebuggerEmulationService emulationService;
protected DebuggerListingService listingService;
protected DebuggerControlService editingService;
protected API api;
protected abstract API newFlatAPI();
@Before
public void setUpFlatAPITest() throws Throwable {
breakpointService = addPlugin(tool, DebuggerLogicalBreakpointServicePlugin.class);
mappingService = tool.getService(DebuggerStaticMappingService.class);
emulationService = addPlugin(tool, DebuggerEmulationServicePlugin.class);
listingService = addPlugin(tool, DebuggerListingPlugin.class);
editingService = addPlugin(tool, DebuggerControlServicePlugin.class);
api = newFlatAPI();
// TODO: This seems to hold up the task manager.
waitForComponentProvider(DebuggerListingProvider.class).setAutoDisassemble(false);
}
@Override
protected void createProgram(Language lang, CompilerSpec cSpec) throws IOException {
super.createProgram(lang, cSpec);
api.getState().setCurrentProgram(program);
}
protected TraceThread createTraceWithThreadAndStack(boolean open) throws Throwable {
if (open) {
createAndOpenTrace();
}
else {
createTrace();
}
TraceThread thread;
try (Transaction tx = tb.startTransaction()) {
thread = tb.getOrAddThread("Threads[0]", 0);
TraceStack stack = tb.trace.getStackManager().getStack(thread, 0, true);
stack.setDepth(3, true);
}
waitForSwing();
return thread;
}
protected void createTraceWithBinText() throws Throwable {
createAndOpenTrace();
try (Transaction tx = tb.startTransaction()) {
DBTraceMemoryManager mm = tb.trace.getMemoryManager();
mm.createRegion("Memory[bin.text]", 0, tb.range(0x00400000, 0x0040ffff),
Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE));
mm.putBytes(0, tb.addr(0x00400000), tb.buf(1, 2, 3, 4, 5, 6, 7, 8));
}
traceManager.activateTrace(tb.trace);
waitForSwing();
}
protected void createMappedTraceAndProgram() throws Throwable {
createAndOpenTrace();
createProgramFromTrace();
intoProject(program);
intoProject(tb.trace);
programManager.openProgram(program);
traceManager.activateTrace(tb.trace);
try (Transaction tx = program.openTransaction("add block")) {
program.getMemory()
.createInitializedBlock(".text", addr(program, 0x00400000), 4096, (byte) 0,
monitor, false);
}
CompletableFuture<Void> changesSettled;
try (Transaction tx = tb.startTransaction()) {
tb.trace.getMemoryManager()
.createRegion("Memory[bin.text]", 0, tb.range(0x00400000, 0x00400fff),
Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE));
changesSettled = mappingService.changesSettled();
mappingService.addIdentityMapping(tb.trace, program, Lifespan.nowOn(0), true);
}
waitForSwing();
waitOn(changesSettled);
}
protected Address createEmulatableProgram() throws Throwable {
createProgram();
programManager.openProgram(program);
Address entry = addr(program, 0x00400000);
try (Transaction start = program.openTransaction("init")) {
program.getMemory()
.createInitializedBlock(".text", entry, 4096, (byte) 0,
monitor, false);
Assembler asm = Assemblers.getAssembler(program);
asm.assemble(entry, "imm r0,#123");
}
// Emulate launch will create a static mapping
intoProject(program);
return entry;
}
@Test
public void testReadMemoryBuffer() throws Throwable {
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
byte[] data = new byte[1024];
assertEquals(1024, api.readMemory(tb.addr(0x00400000), data, monitor));
assertArrayEquals(new byte[1024], data);
}
@Test
public void testReadMemoryLength() throws Throwable {
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
byte[] data = api.readMemory(tb.addr(0x00400000), 1024, monitor);
assertArrayEquals(new byte[1024], data);
}
@Test
public void testReadRegister() throws Throwable {
TraceThread thread = createTraceWithThreadAndStack(true);
traceManager.activateThread(thread);
Register r0 = tb.language.getRegister("r0");
assertEquals(new RegisterValue(r0), api.readRegister("r0"));
}
@Test(expected = IllegalArgumentException.class)
public void testReadRegisterInvalidNameErr() throws Throwable {
TraceThread thread = createTraceWithThreadAndStack(true);
traceManager.activateThread(thread);
api.readRegister("THERE_IS_NO_SUCH_REGISTER");
}
@Test
public void testReadRegisters() throws Throwable {
TraceThread thread = createTraceWithThreadAndStack(true);
traceManager.activateThread(thread);
waitForSwing();
Register r0 = tb.language.getRegister("r0");
Register r1 = tb.language.getRegister("r1");
assertEquals(List.of(
new RegisterValue(r0),
new RegisterValue(r1)),
api.readRegistersNamed(List.of("r0", "r1")));
}
@Test(expected = IllegalArgumentException.class)
public void testReadRegistersInvalidNameErr() throws Throwable {
TraceThread thread = createTraceWithThreadAndStack(true);
traceManager.activateThread(thread);
api.readRegistersNamed(Set.of("THERE_IS_NO_SUCH_REGISTER"));
}
@Test
public void testWriteMemoryGivenContext() throws Throwable {
createTraceWithBinText();
editingService.setCurrentMode(tb.trace, ControlMode.RW_TRACE);
assertTrue(api.writeMemory(tb.trace, 0, tb.addr(0x00400123), tb.arr(3, 2, 1)));
ByteBuffer buf = ByteBuffer.allocate(3);
assertEquals(3, tb.trace.getMemoryManager().getBytes(0, tb.addr(0x00400123), buf));
assertArrayEquals(tb.arr(3, 2, 1), buf.array());
}
@Test
public void testWriteMemoryCurrentContext() throws Throwable {
createTraceWithBinText();
editingService.setCurrentMode(tb.trace, ControlMode.RW_TRACE);
assertTrue(api.writeMemory(tb.addr(0x00400123), tb.arr(3, 2, 1)));
ByteBuffer buf = ByteBuffer.allocate(3);
assertEquals(3, tb.trace.getMemoryManager().getBytes(0, tb.addr(0x00400123), buf));
assertArrayEquals(tb.arr(3, 2, 1), buf.array());
}
@Test
public void testWriteRegisterGivenContext() throws Throwable {
TraceThread thread = createTraceWithThreadAndStack(true);
editingService.setCurrentMode(tb.trace, ControlMode.RW_TRACE);
traceManager.activateThread(thread);
waitForSwing();
assertTrue(api.writeRegister(thread, 0, 0, "r0", BigInteger.valueOf(0x0102030405060708L)));
DBTraceMemorySpace regs =
tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
assertNotNull(regs);
Register r0 = tb.language.getRegister("r0");
assertEquals(new RegisterValue(r0, BigInteger.valueOf(0x0102030405060708L)),
regs.getValue(0, r0));
}
@Test
public void testWriteRegisterCurrentContext() throws Throwable {
TraceThread thread = createTraceWithThreadAndStack(true);
editingService.setCurrentMode(tb.trace, ControlMode.RW_TRACE);
traceManager.activateThread(thread);
waitForSwing();
assertTrue(api.writeRegister("r0", BigInteger.valueOf(0x0102030405060708L)));
DBTraceMemorySpace regs =
tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
assertNotNull(regs);
Register r0 = tb.language.getRegister("r0");
assertEquals(new RegisterValue(r0, BigInteger.valueOf(0x0102030405060708L)),
regs.getValue(0, r0));
}
protected void createProgramWithText() throws Throwable {
createProgram();
programManager.openProgram(program);
waitForSwing();
try (Transaction tx = program.openTransaction("Add block")) {
program.getMemory()
.createInitializedBlock(
".text", addr(program, 0x00400000), 1024, (byte) 0, monitor, false);
}
}
protected void createProgramWithBreakpoint() throws Throwable {
createProgramWithText();
CompletableFuture<Void> changesSettled = breakpointService.changesSettled();
waitOn(breakpointService.placeBreakpointAt(program, addr(program, 0x00400000), 1,
Set.of(TraceBreakpointKind.SW_EXECUTE), "name"));
waitForSwing();
waitOn(changesSettled);
}
}

View File

@ -0,0 +1,101 @@
/* ###
* 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.flatapi;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import org.junit.Test;
import ghidra.app.script.GhidraState;
public abstract class AbstractLiveFlatDebuggerAPITest<API extends FlatDebuggerAPI>
extends AbstractFlatDebuggerAPITest<API> {
protected class TestFlatAPI implements FlatDebuggerAPI {
protected final GhidraState state =
new GhidraState(env.getTool(), env.getProject(), program, null, null, null);
@Override
public GhidraState getState() {
return state;
}
}
protected abstract void runTestResume(BooleanSupplier resume) throws Throwable;
@Test
public void testResumeGivenThread() throws Throwable {
runTestResume(() -> api.resume(api.getCurrentThread()));
}
@Test
public void testResumeGivenTrace() throws Throwable {
runTestResume(() -> api.resume(api.getCurrentTrace()));
}
@Test
public void testResume() throws Throwable {
runTestResume(api::resume);
}
protected abstract void runTestInterrupt(BooleanSupplier interrupt) throws Throwable;
@Test
public void testInterruptGivenThread() throws Throwable {
runTestInterrupt(() -> api.interrupt(api.getCurrentThread()));
}
@Test
public void testInterruptGivenTrace() throws Throwable {
runTestInterrupt(() -> api.interrupt(api.getCurrentTrace()));
}
@Test
public void testInterrupt() throws Throwable {
runTestInterrupt(api::interrupt);
}
protected abstract void runTestKill(BooleanSupplier kill) throws Throwable;
@Test
public void testKillGivenThread() throws Throwable {
runTestKill(() -> api.kill(api.getCurrentThread()));
}
@Test
public void testKillGivenTrace() throws Throwable {
runTestKill(() -> api.kill(api.getCurrentTrace()));
}
@Test
public void testKill() throws Throwable {
runTestKill(api::kill);
}
protected abstract void runTestExecuteCapture(Function<String, String> executeCapture)
throws Throwable;
@Test
public void testExecuteCaptureGivenTrace() throws Throwable {
runTestExecuteCapture(cmd -> api.executeCapture(api.getCurrentTrace(), cmd));
}
@Test
public void testExecuteCapture() throws Throwable {
runTestExecuteCapture(api::executeCapture);
}
}

View File

@ -0,0 +1,539 @@
/* ###
* 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.flatapi;
import static org.junit.Assert.*;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import org.junit.Test;
import db.Transaction;
import generic.Unique;
import ghidra.app.script.GhidraState;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.debug.api.breakpoint.LogicalBreakpoint;
import ghidra.debug.api.breakpoint.LogicalBreakpoint.State;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.program.model.address.Address;
import ghidra.trace.model.Trace;
import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
import ghidra.trace.model.stack.TraceStack;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.schedule.TraceSchedule;
public class DeadFlatDebuggerAPITest extends AbstractFlatDebuggerAPITest<FlatDebuggerAPI> {
protected class TestFlatAPI implements FlatDebuggerAPI {
protected final GhidraState state =
new GhidraState(env.getTool(), env.getProject(), program, null, null, null);
@Override
public GhidraState getState() {
return state;
}
}
@Override
protected FlatDebuggerAPI newFlatAPI() {
return new TestFlatAPI();
}
@Test
public void testRequireService() throws Throwable {
assertEquals(traceManager, api.requireService(DebuggerTraceManagerService.class));
}
interface NoSuchService {
}
@Test(expected = IllegalStateException.class)
public void testRequireServiceAbsentErr() {
api.requireService(NoSuchService.class);
}
@Test
public void testGetCurrentDebuggerCoordinates() throws Throwable {
assertSame(DebuggerCoordinates.NOWHERE, api.getCurrentDebuggerCoordinates());
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
assertEquals(DebuggerCoordinates.NOWHERE.trace(tb.trace),
api.getCurrentDebuggerCoordinates());
}
@Test
public void testGetCurrentTrace() throws Throwable {
assertNull(api.getCurrentTrace());
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
assertEquals(tb.trace, api.getCurrentTrace());
}
@Test(expected = IllegalStateException.class)
public void testRequireCurrentTraceAbsentErr() {
api.requireCurrentTrace();
}
@Test
public void testGetCurrentThread() throws Throwable {
assertNull(api.getCurrentThread());
createAndOpenTrace();
TraceThread thread;
try (Transaction tx = tb.startTransaction()) {
thread = tb.getOrAddThread("Threads[0]", 0);
}
waitForSwing();
traceManager.activateTrace(tb.trace);
assertEquals(thread, api.getCurrentThread());
}
@Test
public void testGetCurrentView() throws Throwable {
assertNull(api.getCurrentView());
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
assertEquals(tb.trace.getProgramView(), api.getCurrentView());
}
@Test(expected = IllegalStateException.class)
public void testRequireCurrentViewAbsentErr() {
api.requireCurrentView();
}
@Test
public void testGetCurrentFrame() throws Throwable {
assertEquals(0, api.getCurrentFrame());
createAndOpenTrace();
TraceThread thread;
try (Transaction tx = tb.startTransaction()) {
thread = tb.getOrAddThread("Threads[0]", 0);
TraceStack stack = tb.trace.getStackManager().getStack(thread, 0, true);
stack.setDepth(3, true);
}
waitForSwing();
traceManager.activateThread(thread);
traceManager.activateFrame(1);
assertEquals(1, api.getCurrentFrame());
}
@Test
public void testGetCurrentSnap() throws Throwable {
assertEquals(0L, api.getCurrentSnap());
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
traceManager.activateSnap(1);
assertEquals(1L, api.getCurrentSnap());
}
@Test
public void testGetCurrentEmulationSchedule() throws Throwable {
assertEquals(TraceSchedule.parse("0"), api.getCurrentEmulationSchedule());
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
traceManager.activateSnap(1);
assertEquals(TraceSchedule.parse("1"), api.getCurrentEmulationSchedule());
}
@Test
public void testActivateTrace() throws Throwable {
createAndOpenTrace();
api.activateTrace(tb.trace);
assertEquals(tb.trace, traceManager.getCurrentTrace());
}
@Test
public void testActivateTraceNull() throws Throwable {
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
waitForSwing();
assertEquals(tb.trace, traceManager.getCurrentTrace());
api.activateTrace(null);
assertEquals(null, traceManager.getCurrentTrace());
}
@Test
public void testActivateTraceNotOpen() throws Throwable {
createTrace();
assertFalse(traceManager.getOpenTraces().contains(tb.trace));
api.activateTrace(tb.trace);
assertTrue(traceManager.getOpenTraces().contains(tb.trace));
assertEquals(tb.trace, traceManager.getCurrentTrace());
}
@Test
public void testGetCurrentProgram() throws Throwable {
assertEquals(null, api.getCurrentProgram());
createProgram();
programManager.openProgram(program);
assertEquals(program, api.getCurrentProgram());
}
@Test(expected = IllegalStateException.class)
public void testRequireCurrentProgramAbsentErr() throws Throwable {
api.requireCurrentProgram();
}
@Test
public void testActivateThread() throws Throwable {
TraceThread thread = createTraceWithThreadAndStack(true);
api.activateThread(thread);
assertEquals(thread, traceManager.getCurrentThread());
}
@Test
public void testActivateThreadNull() throws Throwable {
api.activateThread(null);
assertEquals(null, traceManager.getCurrentThread());
TraceThread thread = createTraceWithThreadAndStack(true);
traceManager.activateThread(thread);
waitForSwing();
assertEquals(thread, traceManager.getCurrentThread());
api.activateThread(null);
assertNull(traceManager.getCurrentThread());
}
@Test
public void testActivateThreadNotOpen() throws Throwable {
TraceThread thread = createTraceWithThreadAndStack(false);
assertFalse(traceManager.getOpenTraces().contains(tb.trace));
api.activateThread(thread);
assertTrue(traceManager.getOpenTraces().contains(tb.trace));
assertEquals(thread, traceManager.getCurrentThread());
}
@Test
public void testActivateFrame() throws Throwable {
TraceThread thread = createTraceWithThreadAndStack(true);
traceManager.activateThread(thread);
waitForSwing();
api.activateFrame(1);
assertEquals(1, traceManager.getCurrentFrame());
}
@Test
public void testActivateSnap() throws Throwable {
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
waitForSwing();
api.activateSnap(1);
assertEquals(1L, traceManager.getCurrentSnap());
}
@Test
public void testGetCurrentDebuggerAddress() throws Throwable {
assertEquals(null, api.getCurrentDebuggerAddress());
createTraceWithBinText();
assertEquals(tb.addr(0x00400000), api.getCurrentDebuggerAddress());
}
@Test
public void testGoToDynamic() throws Throwable {
createTraceWithBinText();
assertTrue(api.goToDynamic("00400123"));
assertEquals(tb.addr(0x00400123), listingService.getCurrentLocation().getAddress());
assertTrue(api.goToDynamic(tb.addr(0x00400321)));
assertEquals(tb.addr(0x00400321), listingService.getCurrentLocation().getAddress());
}
@Test
public void testTranslateStaticToDynamic() throws Throwable {
createMappedTraceAndProgram();
assertEquals(api.dynamicLocation("00400123"),
api.translateStaticToDynamic(api.staticLocation("00400123")));
assertNull(api.translateStaticToDynamic(api.staticLocation("00600123")));
assertEquals(tb.addr(0x00400123), api.translateStaticToDynamic(addr(program, 0x00400123)));
assertNull(api.translateStaticToDynamic(addr(program, 0x00600123)));
}
@Test
public void testTranslateDynamicToStatic() throws Throwable {
createMappedTraceAndProgram();
assertEquals(api.staticLocation("00400123"),
api.translateDynamicToStatic(api.dynamicLocation("00400123")));
assertNull(api.translateDynamicToStatic(api.dynamicLocation("00600123")));
assertEquals(addr(program, 0x00400123), api.translateDynamicToStatic(tb.addr(0x00400123)));
assertNull(api.translateDynamicToStatic(tb.addr(0x00600123)));
}
@Test
public void testEmulateLaunch() throws Throwable {
Address entry = createEmulatableProgram();
Trace trace = api.emulateLaunch(entry);
assertEquals(trace, traceManager.getCurrentTrace());
}
@Test
public void testEmulate() throws Throwable {
Address entry = createEmulatableProgram();
api.emulateLaunch(entry);
TraceSchedule schedule =
traceManager.getCurrent().getTime().steppedForward(traceManager.getCurrentThread(), 1);
api.emulate(schedule, monitor);
assertEquals(schedule, traceManager.getCurrent().getTime());
}
@Test
public void testStepEmuInstruction() throws Throwable {
Address entry = createEmulatableProgram();
api.emulateLaunch(entry);
TraceSchedule schedule =
traceManager.getCurrent().getTime().steppedForward(traceManager.getCurrentThread(), 1);
api.stepEmuInstruction(1, monitor);
assertEquals(schedule, traceManager.getCurrent().getTime());
api.stepEmuInstruction(-1, monitor);
assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime());
}
@Test
public void testStepEmuPcodeOp() throws Throwable {
Address entry = createEmulatableProgram();
api.emulateLaunch(entry);
TraceSchedule schedule = traceManager.getCurrent()
.getTime()
.steppedPcodeForward(traceManager.getCurrentThread(), 1);
api.stepEmuPcodeOp(1, monitor);
assertEquals(schedule, traceManager.getCurrent().getTime());
api.stepEmuPcodeOp(-1, monitor);
assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime());
}
@Test
public void testSkipEmuInstruction() throws Throwable {
Address entry = createEmulatableProgram();
api.emulateLaunch(entry);
TraceSchedule schedule =
traceManager.getCurrent().getTime().skippedForward(traceManager.getCurrentThread(), 1);
api.skipEmuInstruction(1, monitor);
assertEquals(schedule, traceManager.getCurrent().getTime());
api.skipEmuInstruction(-1, monitor);
assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime());
}
@Test
public void testSkipEmuPcodeOp() throws Throwable {
Address entry = createEmulatableProgram();
api.emulateLaunch(entry);
TraceSchedule schedule = traceManager.getCurrent()
.getTime()
.skippedPcodeForward(traceManager.getCurrentThread(), 1);
api.skipEmuPcodeOp(1, monitor);
assertEquals(schedule, traceManager.getCurrent().getTime());
api.skipEmuPcodeOp(-1, monitor);
assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime());
}
@Test
public void testPatchEmu() throws Throwable {
Address entry = createEmulatableProgram();
api.emulateLaunch(entry);
TraceSchedule schedule = traceManager.getCurrent()
.getTime()
.patched(traceManager.getCurrentThread(),
traceManager.getCurrentPlatform().getLanguage(), "r0=0x321");
api.patchEmu("r0=0x321", monitor);
assertEquals(schedule, traceManager.getCurrent().getTime());
api.stepEmuInstruction(-1, monitor);
assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime());
}
@Test
public void testSearchMemory() throws Throwable {
createTraceWithBinText();
traceManager.activateTrace(tb.trace);
waitForSwing();
assertEquals(tb.addr(0x00400003), api.searchMemory(tb.trace, 2, tb.range(0L, -1L),
tb.arr(4, 5, 6, 7), null, true, monitor));
assertEquals(tb.addr(0x00400003), api.searchMemory(tb.trace, 2, tb.range(0L, -1L),
tb.arr(4, 5, 6, 7), tb.arr(-1, -1, -1, -1), true, monitor));
}
@Test
public void testGetAllBreakpoints() throws Throwable {
createProgramWithBreakpoint();
assertEquals(1, api.getAllBreakpoints().size());
}
@Test
public void testGetBreakpointsAt() throws Throwable {
createProgramWithBreakpoint();
assertEquals(1, api.getBreakpointsAt(api.staticLocation("00400000")).size());
assertEquals(0, api.getBreakpointsAt(api.staticLocation("00400001")).size());
}
@Test
public void testGetBreakpointsNamed() throws Throwable {
createProgramWithBreakpoint();
assertEquals(1, api.getBreakpointsNamed("name").size());
assertEquals(0, api.getBreakpointsNamed("miss").size());
}
@Test
public void testBreakpointsToggle() throws Throwable {
createProgramWithBreakpoint();
LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints());
assertEquals(State.INEFFECTIVE_ENABLED, lb.computeState());
assertEquals(Set.of(lb), api.breakpointsToggle(api.staticLocation("00400000")));
assertEquals(State.INEFFECTIVE_DISABLED, lb.computeState());
}
@Test
public void testBreakpointSetSoftwareExecute() throws Throwable {
createProgramWithText();
LogicalBreakpoint lb = Unique.assertOne(
api.breakpointSetSoftwareExecute(api.staticLocation("00400000"), "name"));
assertEquals(addr(program, 0x00400000), lb.getAddress());
assertEquals(TraceBreakpointKindSet.SW_EXECUTE, lb.getKinds());
assertEquals(1, lb.getLength());
}
@Test
public void testBreakpointSetHardwareExecute() throws Throwable {
createProgramWithText();
LogicalBreakpoint lb = Unique.assertOne(
api.breakpointSetHardwareExecute(api.staticLocation("00400000"), "name"));
assertEquals(addr(program, 0x00400000), lb.getAddress());
assertEquals(TraceBreakpointKindSet.HW_EXECUTE, lb.getKinds());
assertEquals(1, lb.getLength());
}
@Test
public void testBreakpointSetRead() throws Throwable {
createProgramWithText();
LogicalBreakpoint lb = Unique.assertOne(
api.breakpointSetRead(api.staticLocation("00400000"), 4, "name"));
assertEquals(addr(program, 0x00400000), lb.getAddress());
assertEquals(TraceBreakpointKindSet.READ, lb.getKinds());
assertEquals(4, lb.getLength());
}
@Test
public void testBreakpointSetWrite() throws Throwable {
createProgramWithText();
LogicalBreakpoint lb = Unique.assertOne(
api.breakpointSetWrite(api.staticLocation("00400000"), 4, "name"));
assertEquals(addr(program, 0x00400000), lb.getAddress());
assertEquals(TraceBreakpointKindSet.WRITE, lb.getKinds());
assertEquals(4, lb.getLength());
}
@Test
public void testBreakpointSetAccess() throws Throwable {
createProgramWithText();
LogicalBreakpoint lb = Unique.assertOne(
api.breakpointSetAccess(api.staticLocation("00400000"), 4, "name"));
assertEquals(addr(program, 0x00400000), lb.getAddress());
assertEquals(TraceBreakpointKindSet.ACCESS, lb.getKinds());
assertEquals(4, lb.getLength());
}
@Test
public void testBreakpointsEnable() throws Throwable {
createProgramWithBreakpoint();
LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints());
CompletableFuture<Void> changesSettled = breakpointService.changesSettled();
waitOn(lb.disable());
waitForSwing();
waitOn(changesSettled);
assertEquals(State.INEFFECTIVE_DISABLED, lb.computeState());
assertEquals(Set.of(lb), api.breakpointsEnable(api.staticLocation("00400000")));
assertEquals(State.INEFFECTIVE_ENABLED, lb.computeState());
}
@Test
public void testBreakpointsDisable() throws Throwable {
createProgramWithBreakpoint();
LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints());
assertEquals(State.INEFFECTIVE_ENABLED, lb.computeState());
assertEquals(Set.of(lb), api.breakpointsDisable(api.staticLocation("00400000")));
assertEquals(State.INEFFECTIVE_DISABLED, lb.computeState());
}
@Test
public void testBreakpointsClear() throws Throwable {
createProgramWithBreakpoint();
LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints());
assertTrue(api.breakpointsClear(api.staticLocation("00400000")));
assertTrue(lb.isEmpty());
assertEquals(0, breakpointService.getAllBreakpoints().size());
}
}

View File

@ -0,0 +1,502 @@
/* ###
* 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.flatapi;
import static org.junit.Assert.*;
import java.io.IOException;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import org.junit.Test;
import generic.Unique;
import ghidra.app.plugin.core.debug.service.model.TestDebuggerProgramLaunchOpinion.TestDebuggerProgramLaunchOffer;
import ghidra.app.plugin.core.debug.service.model.launch.AbstractDebuggerProgramLaunchOffer;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.model.*;
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetSteppable.TargetStepKind;
import ghidra.debug.api.model.DebuggerProgramLaunchOffer;
import ghidra.debug.api.model.DebuggerProgramLaunchOffer.LaunchResult;
import ghidra.debug.api.model.TraceRecorder;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.lang.*;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Trace;
import ghidra.trace.model.thread.TraceThread;
public class FlatDebuggerRecorderAPITest
extends AbstractLiveFlatDebuggerAPITest<FlatDebuggerRecorderAPI> {
protected static class TestFactory implements DebuggerModelFactory {
private final DebuggerObjectModel model;
public TestFactory(DebuggerObjectModel model) {
this.model = model;
}
@Override
public CompletableFuture<? extends DebuggerObjectModel> build() {
return CompletableFuture.completedFuture(model);
}
}
protected class TestOffer extends AbstractDebuggerProgramLaunchOffer {
public TestOffer(Program program, DebuggerModelFactory factory) {
super(program, env.getTool(), factory);
}
public TestOffer(Program program, DebuggerObjectModel model) {
this(program, new TestFactory(model));
}
@Override
public String getConfigName() {
return "TEST";
}
@Override
public String getMenuTitle() {
return "in Test Debugger";
}
}
protected static class TestModelBuilder extends TestDebuggerModelBuilder {
private final TestDebuggerObjectModel model;
public TestModelBuilder(TestDebuggerObjectModel model) {
this.model = model;
}
@Override
protected TestDebuggerObjectModel newModel(String typeHint) {
return model;
}
}
protected class TestFlatRecorderAPI extends TestFlatAPI implements FlatDebuggerRecorderAPI {
}
@Override
protected FlatDebuggerRecorderAPI newFlatAPI() {
return new TestFlatRecorderAPI();
}
protected TraceRecorder recordTarget(TargetObject target)
throws LanguageNotFoundException, CompilerSpecNotFoundException, IOException {
return modelService.recordTargetAndActivateTrace(target,
new TestDebuggerTargetTraceMapper(target));
}
@Test
public void testReadLiveMemory() throws Throwable {
createTestModel();
mb.createTestProcessesAndThreads();
mb.testProcess1.memory.writeMemory(mb.addr(0x00400000), mb.arr(1, 2, 3, 4, 5, 6, 7, 8));
waitOn(mb.testModel.flushEvents());
TraceRecorder recorder = recordTarget(mb.testProcess1);
waitRecorder(recorder);
useTrace(recorder.getTrace());
waitForSwing();
byte[] data = api.readMemory(tb.addr(0x00400000), 8, monitor);
assertArrayEquals(tb.arr(1, 2, 3, 4, 5, 6, 7, 8), data);
}
@Test
public void testReadLiveRegister() throws Throwable {
createTestModel();
mb.createTestProcessesAndThreads();
mb.createTestThreadRegisterBanks();
mb.testProcess1.regs.addRegistersFromLanguage(getToyBE64Language(), r -> true);
mb.testBank1.writeRegister("r0", mb.arr(1, 2, 3, 4, 5, 6, 7, 8));
waitOn(mb.testModel.flushEvents());
TraceRecorder recorder = recordTarget(mb.testProcess1);
waitRecorder(recorder);
useTrace(recorder.getTrace());
traceManager.activateThread(recorder.getTraceThread(mb.testThread1));
waitForSwing();
RegisterValue rv = api.readRegister("r0");
assertEquals(BigInteger.valueOf(0x0102030405060708L), rv.getUnsignedValue());
}
@Test
public void testReadLiveRegisters() throws Throwable {
createTestModel();
mb.createTestProcessesAndThreads();
mb.createTestThreadRegisterBanks();
mb.testProcess1.regs.addRegistersFromLanguage(getToyBE64Language(), r -> true);
mb.testBank1.writeRegister("r0", mb.arr(1, 2, 3, 4, 5, 6, 7, 8));
mb.testBank1.writeRegister("r1", mb.arr(8, 7, 6, 5, 4, 3, 2, 1));
waitOn(mb.testModel.flushEvents());
TraceRecorder recorder = recordTarget(mb.testProcess1);
waitRecorder(recorder);
useTrace(recorder.getTrace());
traceManager.activateThread(recorder.getTraceThread(mb.testThread1));
waitForSwing();
Register r0 = tb.language.getRegister("r0");
Register r1 = tb.language.getRegister("r1");
assertEquals(List.of(
new RegisterValue(r0, BigInteger.valueOf(0x0102030405060708L)),
new RegisterValue(r1, BigInteger.valueOf(0x0807060504030201L))),
api.readRegistersNamed(List.of("r0", "r1")));
}
@Test
public void testGetLaunchOffers() throws Throwable {
createProgram();
programManager.openProgram(program);
waitForSwing();
Unique.assertOne(api.getLaunchOffers().stream().mapMulti((o, m) -> {
if (o instanceof TestDebuggerProgramLaunchOffer to) {
m.accept(to);
}
}));
}
@Test
public void testLaunchCustomCommandLine() throws Throwable {
createProgram();
programManager.openProgram(program);
waitForSwing();
var model = new TestDebuggerObjectModel() {
Map<String, ?> observedParams;
@Override
protected TestTargetSession newTestTargetSession(String rootHint) {
return new TestTargetSession(this, "Session", ROOT_SCHEMA) {
@Override
public CompletableFuture<Void> launch(Map<String, ?> params) {
observedParams = params;
throw new CancellationException();
}
};
}
};
DebuggerProgramLaunchOffer offer = new TestOffer(program, model);
LaunchResult result = api.launch(offer, "custom command line", monitor);
assertEquals("custom command line",
model.observedParams.get(TargetCmdLineLauncher.CMDLINE_ARGS_NAME));
assertNotNull(result.model());
assertNull(result.target());
assertEquals(CancellationException.class, result.exception().getClass());
}
@Test
public void testGetTarget() throws Exception {
createTestModel();
mb.createTestProcessesAndThreads();
TraceRecorder recorder = recordTarget(mb.testProcess1);
assertEquals(mb.testProcess1, api.getTarget(recorder.getTrace()));
}
@Test
public void testGetTargetThread() throws Throwable {
createTestModel();
mb.createTestProcessesAndThreads();
TraceRecorder recorder = recordTarget(mb.testProcess1);
waitRecorder(recorder);
Trace trace = recorder.getTrace();
TraceThread thread =
trace.getThreadManager()
.getLiveThreadByPath(recorder.getSnap(), "Processes[1].Threads[1]");
assertNotNull(thread);
assertEquals(mb.testThread1, api.getTargetThread(thread));
}
@Test
public void testGetTargetFocus() throws Throwable {
createTestModel();
mb.createTestProcessesAndThreads();
TraceRecorder recorder = recordTarget(mb.testProcess1);
waitRecorder(recorder);
waitOn(mb.testModel.requestFocus(mb.testThread2));
waitRecorder(recorder);
assertEquals(mb.testThread2, api.getTargetFocus(recorder.getTrace()));
}
protected void runTestStep(BooleanSupplier step, TargetStepKind kind) throws Throwable {
var model = new TestDebuggerObjectModel() {
TestTargetThread observedThread;
TargetStepKind observedKind;
@Override
protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container,
int tid) {
return new TestTargetThread(container, tid) {
@Override
public CompletableFuture<Void> step(TargetStepKind kind) {
observedThread = this;
observedKind = kind;
return super.step(kind);
}
};
}
};
mb = new TestModelBuilder(model);
createTestModel();
mb.createTestProcessesAndThreads();
TraceRecorder recorder = recordTarget(mb.testProcess1);
waitRecorder(recorder);
assertTrue(waitOn(recorder.requestFocus(mb.testThread2)));
waitRecorder(recorder);
assertTrue(step.getAsBoolean());
waitRecorder(recorder);
assertEquals(mb.testThread2, model.observedThread);
assertEquals(kind, model.observedKind);
}
@Test
public void testStepGivenThread() throws Throwable {
runTestStep(() -> api.step(api.getCurrentThread(), TargetStepKind.INTO),
TargetStepKind.INTO);
}
@Test
public void testStepInto() throws Throwable {
runTestStep(api::stepInto, TargetStepKind.INTO);
}
@Test
public void testStepOver() throws Throwable {
runTestStep(api::stepOver, TargetStepKind.OVER);
}
@Test
public void testStepOut() throws Throwable {
runTestStep(api::stepOut, TargetStepKind.FINISH);
}
@Override
protected void runTestResume(BooleanSupplier resume) throws Throwable {
var model = new TestDebuggerObjectModel() {
TestTargetThread observedThread;
@Override
protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container,
int tid) {
return new TestTargetThread(container, tid) {
@Override
public CompletableFuture<Void> resume() {
observedThread = this;
return super.resume();
}
};
}
};
mb = new TestModelBuilder(model);
createTestModel();
mb.createTestProcessesAndThreads();
TraceRecorder recorder = recordTarget(mb.testProcess1);
waitRecorder(recorder);
assertTrue(waitOn(recorder.requestFocus(mb.testThread2)));
waitRecorder(recorder);
assertTrue(resume.getAsBoolean());
waitRecorder(recorder);
assertEquals(mb.testThread2, model.observedThread);
}
@Override
protected void runTestInterrupt(BooleanSupplier interrupt) throws Throwable {
var model = new TestDebuggerObjectModel() {
TestTargetThread observedThread;
@Override
protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container,
int tid) {
return new TestTargetThread(container, tid) {
@Override
public CompletableFuture<Void> interrupt() {
observedThread = this;
return super.interrupt();
}
};
}
};
mb = new TestModelBuilder(model);
createTestModel();
mb.createTestProcessesAndThreads();
TraceRecorder recorder = recordTarget(mb.testProcess1);
waitRecorder(recorder);
assertTrue(waitOn(recorder.requestFocus(mb.testThread2)));
waitRecorder(recorder);
assertTrue(interrupt.getAsBoolean());
waitRecorder(recorder);
assertEquals(mb.testThread2, model.observedThread);
}
@Override
protected void runTestKill(BooleanSupplier kill) throws Throwable {
var model = new TestDebuggerObjectModel() {
TestTargetThread observedThread;
@Override
protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container,
int tid) {
return new TestTargetThread(container, tid) {
@Override
public CompletableFuture<Void> kill() {
observedThread = this;
return super.kill();
}
};
}
};
mb = new TestModelBuilder(model);
createTestModel();
mb.createTestProcessesAndThreads();
TraceRecorder recorder = recordTarget(mb.testProcess1);
waitRecorder(recorder);
assertTrue(waitOn(recorder.requestFocus(mb.testThread2)));
waitRecorder(recorder);
assertTrue(kill.getAsBoolean());
waitRecorder(recorder);
assertEquals(mb.testThread2, model.observedThread);
}
@Override
protected void runTestExecuteCapture(Function<String, String> executeCapture) throws Throwable {
// NOTE: Can't use TestTargetInterpreter.queueExecute stuff, since flat API waits
var model = new TestDebuggerObjectModel() {
@Override
protected TestTargetInterpreter newTestTargetInterpreter(TestTargetSession session) {
return new TestTargetInterpreter(session) {
@Override
public CompletableFuture<String> executeCapture(String cmd) {
return CompletableFuture.completedFuture("Response to " + cmd);
}
};
}
};
mb = new TestModelBuilder(model);
createTestModel();
mb.createTestProcessesAndThreads();
TraceRecorder recorder = recordTarget(mb.testProcess1);
waitRecorder(recorder);
assertTrue(waitOn(recorder.requestFocus(mb.testThread2)));
waitRecorder(recorder);
assertEquals("Response to cmd", executeCapture.apply("cmd"));
}
@Test
public void testGetModelValue() throws Throwable {
createTestModel();
mb.createTestProcessesAndThreads();
recordTarget(mb.testProcess1);
assertEquals(mb.testThread2, api.getModelValue("Processes[1].Threads[2]"));
}
@Test
public void testRefreshObjectChildren() throws Throwable {
var model = new TestDebuggerObjectModel() {
Set<TestTargetProcess> observed = new HashSet<>();
@Override
protected TestTargetProcess newTestTargetProcess(TestTargetProcessContainer container,
int pid, AddressSpace space) {
return new TestTargetProcess(container, pid, space) {
@Override
public CompletableFuture<Void> resync(RefreshBehavior refreshAttributes,
RefreshBehavior refreshElements) {
observed.add(this);
return super.resync(refreshAttributes, refreshElements);
}
};
}
};
mb = new TestModelBuilder(model);
createTestModel();
mb.createTestProcessesAndThreads();
api.refreshObjectChildren(mb.testProcess1);
assertEquals(Set.of(mb.testProcess1), model.observed);
}
@Test
public void testRefreshSubtree() throws Throwable {
var model = new TestDebuggerObjectModel() {
Set<TestTargetObject> observed = new HashSet<>();
@Override
protected TestTargetProcess newTestTargetProcess(TestTargetProcessContainer container,
int pid, AddressSpace space) {
return new TestTargetProcess(container, pid, space) {
@Override
public CompletableFuture<Void> resync(RefreshBehavior refreshAttributes,
RefreshBehavior refreshElements) {
observed.add(this);
return super.resync(refreshAttributes, refreshElements);
}
};
}
@Override
protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container,
int tid) {
return new TestTargetThread(container, tid) {
@Override
public CompletableFuture<Void> resync(RefreshBehavior refreshAttributes,
RefreshBehavior refreshElements) {
observed.add(this);
return super.resync(refreshAttributes, refreshElements);
}
};
}
};
mb = new TestModelBuilder(model);
createTestModel();
mb.createTestProcessesAndThreads();
api.refreshSubtree(mb.testModel.session);
assertEquals(Set.of(mb.testProcess1, mb.testProcess3, mb.testThread1, mb.testThread2,
mb.testThread3, mb.testThread4), model.observed);
}
@Test
public void testFlushAsyncPipelines() throws Throwable {
createTestModel();
mb.createTestProcessesAndThreads();
TraceRecorder recorder = recordTarget(mb.testProcess1);
// Ensure it works whether or not there are pending events
for (int i = 0; i < 10; i++) {
api.flushAsyncPipelines(recorder.getTrace());
}
}
}

View File

@ -0,0 +1,282 @@
/* ###
* 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.flatapi;
import static org.junit.Assert.*;
import java.math.BigInteger;
import java.util.*;
import java.util.function.*;
import org.junit.Before;
import org.junit.Test;
import db.Transaction;
import generic.Unique;
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TestTraceRmiLaunchOpinion.TestTraceRmiLaunchOffer;
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLauncherServicePlugin;
import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiConnection.TestRemoteMethod;
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiTarget;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.LaunchResult;
import ghidra.program.model.lang.Register;
import ghidra.program.model.lang.RegisterValue;
import ghidra.trace.database.memory.DBTraceMemorySpace;
import ghidra.trace.database.target.DBTraceObjectManager;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.thread.TraceObjectThread;
import ghidra.trace.model.thread.TraceThread;
public class FlatDebuggerRmiAPITest extends AbstractLiveFlatDebuggerAPITest<FlatDebuggerRmiAPI> {
protected TraceRmiLauncherServicePlugin rmiLaunchPlugin;
protected class TestFlatRmiAPI extends TestFlatAPI implements FlatDebuggerRmiAPI {
}
@Before
public void setUpRmiTest() throws Throwable {
rmiLaunchPlugin = addPlugin(tool, TraceRmiLauncherServicePlugin.class);
}
@Override
protected FlatDebuggerRmiAPI newFlatAPI() {
return new TestFlatRmiAPI();
}
protected TraceRmiTarget createTarget() throws Throwable {
createRmiConnection();
addExecuteMethod();
addControlMethods();
addMemoryMethods();
addRegisterMethods();
createTrace();
try (Transaction tx = tb.startTransaction()) {
DBTraceObjectManager objs = tb.trace.getObjectManager();
objs.createRootObject(SCHEMA_SESSION);
tb.createObjectsProcessAndThreads();
tb.createObjectsFramesAndRegs(
tb.obj("Processes[1].Threads[1]").queryInterface(TraceObjectThread.class),
Lifespan.nowOn(0), tb.host, 2);
addMemoryRegion(objs, Lifespan.nowOn(0), tb.range(0x00400000, 0x00400fff), ".text",
"rx");
}
TraceRmiTarget target = rmiCx.publishTarget(tool, tb.trace);
traceManager.openTrace(tb.trace);
// Do not activate, as this pollutes the method invocation queues
waitForSwing();
return target;
}
@Test
public void testReadLiveMemory() throws Throwable {
TraceRmiTarget target = createTarget();
var args = rmiMethodReadMem.expect(a -> {
try (Transaction tx = tb.startTransaction()) {
tb.trace.getMemoryManager()
.putBytes(target.getSnap(), tb.addr(0x00400000),
tb.buf(1, 2, 3, 4, 5, 6, 7, 8));
}
return null;
});
byte[] data = api.readMemory(tb.trace, target.getSnap(), tb.addr(0x00400000), 8, monitor);
assertEquals(Map.ofEntries(
Map.entry("process", tb.obj("Processes[1]")),
// Framework quantizes to page
Map.entry("range", tb.range(0x00400000, 0x00400fff))),
waitOn(args));
assertArrayEquals(tb.arr(1, 2, 3, 4, 5, 6, 7, 8), data);
}
@Test
public void testReadLiveRegister() throws Throwable {
TraceRmiTarget target = createTarget();
TraceThread thread =
tb.trace.getThreadManager().getLiveThreadByPath(0, "Processes[1].Threads[1]");
Register r0 = tb.reg("r0");
var args = rmiMethodReadRegs.expect(a -> {
try (Transaction tx = tb.startTransaction()) {
DBTraceMemorySpace regs =
tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, true);
regs.setValue(target.getSnap(), new RegisterValue(r0, new BigInteger("1234")));
}
return null;
});
RegisterValue value = api.readRegister(tb.host, thread, 0, target.getSnap(), r0);
assertEquals(Map.ofEntries(
Map.entry("container", tb.obj("Processes[1].Threads[1].Stack[0].Registers"))),
waitOn(args));
assertEquals(new RegisterValue(r0, new BigInteger("1234")), value);
}
@Test
public void testReadLiveRegisters() throws Throwable {
TraceRmiTarget target = createTarget();
TraceThread thread =
tb.trace.getThreadManager().getLiveThreadByPath(0, "Processes[1].Threads[1]");
Register r0 = tb.reg("r0");
Register r1 = tb.reg("r1");
var args = rmiMethodReadRegs.expect(a -> {
try (Transaction tx = tb.startTransaction()) {
DBTraceMemorySpace regs =
tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, true);
regs.setValue(target.getSnap(), new RegisterValue(r0, new BigInteger("1234")));
regs.setValue(target.getSnap(), new RegisterValue(r1, new BigInteger("5678")));
}
return null;
});
List<RegisterValue> values =
api.readRegisters(tb.host, thread, 0, target.getSnap(), List.of(r0, r1));
assertEquals(Map.ofEntries(
Map.entry("container", tb.obj("Processes[1].Threads[1].Stack[0].Registers"))),
waitOn(args));
assertEquals(List.of(
new RegisterValue(r0, new BigInteger("1234")),
new RegisterValue(r1, new BigInteger("5678"))),
values);
}
protected <T, U extends T> List<U> filter(Collection<T> col, Class<U> cls) {
return col.stream().<U> mapMulti((e, m) -> {
if (cls.isInstance(e)) {
m.accept(cls.cast(e));
}
}).toList();
}
@Test
public void testGetLaunchOffers() throws Throwable {
createProgram();
programManager.openProgram(program);
waitForSwing();
TestTraceRmiLaunchOffer offer =
Unique.assertOne(filter(api.getLaunchOffers(), TestTraceRmiLaunchOffer.class));
assertEquals(program, offer.getProgram());
}
@Test
public void testGetSavedLaunchOffers() throws Throwable {
createProgram();
programManager.openProgram(program);
waitForSwing();
assertEquals(List.of(), api.getSavedLaunchOffers());
TestTraceRmiLaunchOffer offer =
Unique.assertOne(filter(api.getLaunchOffers(), TestTraceRmiLaunchOffer.class));
offer.saveLauncherArgs(Map.of("image", "/test/image"));
assertEquals(List.of(offer), api.getSavedLaunchOffers());
}
@Test
public void testLaunchCustomCommandLine() throws Throwable {
TestTraceRmiLaunchOffer offer =
Unique.assertOne(filter(api.getLaunchOffers(), TestTraceRmiLaunchOffer.class));
offer.saveLauncherArgs(Map.of("image", "/test/image"));
LaunchResult result = api.launch(monitor);
assertEquals("Test launcher cannot launch /test/image", result.exception().getMessage());
}
protected void runTestStep(Predicate<TraceThread> step, Supplier<TestRemoteMethod> method)
throws Throwable {
createTarget();
TraceObjectThread thread =
tb.obj("Processes[1].Threads[1]").queryInterface(TraceObjectThread.class);
traceManager.activateThread(thread);
waitForSwing();
var args = method.get().expect(a -> null);
assertTrue(step.test(thread));
assertEquals(Map.ofEntries(
Map.entry("thread", thread.getObject())),
waitOn(args));
}
@Test
public void testStepGivenThread() throws Throwable {
runTestStep(api::stepInto, () -> rmiMethodStepInto);
}
@Test
public void testStepInto() throws Throwable {
runTestStep(t -> api.stepInto(), () -> rmiMethodStepInto);
}
@Test
public void testStepOver() throws Throwable {
runTestStep(t -> api.stepOver(), () -> rmiMethodStepOver);
}
@Test
public void testStepOut() throws Throwable {
runTestStep(t -> api.stepOut(), () -> rmiMethodStepOut);
}
@Override
protected void runTestResume(BooleanSupplier resume) throws Throwable {
createTarget();
traceManager.activateTrace(tb.trace);
waitForSwing();
var args = rmiMethodResume.expect(a -> null);
assertTrue(resume.getAsBoolean());
assertEquals(Map.ofEntries(
Map.entry("process", tb.obj("Processes[1]"))),
waitOn(args));
}
@Override
protected void runTestInterrupt(BooleanSupplier interrupt) throws Throwable {
createTarget();
traceManager.activateTrace(tb.trace);
waitForSwing();
var args = rmiMethodInterrupt.expect(a -> null);
assertTrue(interrupt.getAsBoolean());
assertEquals(Map.ofEntries(
Map.entry("process", tb.obj("Processes[1]"))),
waitOn(args));
}
@Override
protected void runTestKill(BooleanSupplier kill) throws Throwable {
createTarget();
traceManager.activateTrace(tb.trace);
waitForSwing();
var args = rmiMethodKill.expect(a -> null);
assertTrue(kill.getAsBoolean());
assertEquals(Map.ofEntries(
Map.entry("process", tb.obj("Processes[1]"))),
waitOn(args));
}
@Override
protected void runTestExecuteCapture(Function<String, String> executeCapture) throws Throwable {
createTarget();
traceManager.activateTrace(tb.trace);
waitForSwing();
var args = rmiMethodExecute.expect(a -> "result");
assertEquals("result", api.executeCapture("some command"));
assertEquals(Map.ofEntries(
Map.entry("cmd", "some command"),
Map.entry("to_string", true)),
waitOn(args));
}
}