mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 02:39:44 +02:00
GP-1808: Added 'Run to Address'-type actions to right-click menu for some connectors.
This commit is contained in:
parent
44d7c4f031
commit
bde529b4d5
39 changed files with 1663 additions and 136 deletions
|
@ -612,9 +612,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
|||
|
||||
public DebuggerControlPlugin(PluginTool tool) {
|
||||
super(tool);
|
||||
|
||||
tool.addContextListener(this);
|
||||
|
||||
createActions();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.control;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.Tool;
|
||||
import docking.action.*;
|
||||
import docking.actions.PopupActionProvider;
|
||||
import ghidra.app.context.ProgramLocationActionContext;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
|
||||
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.dbg.target.TargetMethod;
|
||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.util.PathPredicates;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.util.MarkerLocation;
|
||||
import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.trace.model.program.TraceProgramView;
|
||||
import ghidra.trace.model.target.TraceObject;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
@PluginInfo(
|
||||
shortDescription = "Debugger model method actions",
|
||||
description = "Adds context actions to the GUI, generically, based on the model's methods",
|
||||
category = PluginCategoryNames.DEBUGGER,
|
||||
packageName = DebuggerPluginPackage.NAME,
|
||||
status = PluginStatus.RELEASED,
|
||||
eventsConsumed = {
|
||||
},
|
||||
servicesRequired = {
|
||||
DebuggerStaticMappingService.class,
|
||||
})
|
||||
public class DebuggerMethodActionsPlugin extends Plugin implements PopupActionProvider {
|
||||
public static final String GROUP_METHODS = "Debugger Methods";
|
||||
|
||||
private static String getDisplay(TargetMethod method) {
|
||||
String display = method.getDisplay();
|
||||
if (display != null) {
|
||||
return display;
|
||||
}
|
||||
return method.getName();
|
||||
}
|
||||
|
||||
class InvokeMethodAction extends DockingAction {
|
||||
private final TargetMethod method;
|
||||
|
||||
public InvokeMethodAction(TargetMethod method) {
|
||||
super(getDisplay(method), DebuggerMethodActionsPlugin.this.getName());
|
||||
this.method = method;
|
||||
setPopupMenuData(new MenuData(new String[] { getName() }, GROUP_METHODS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionContext context) {
|
||||
Map<String, Object> arguments = collectArguments(method.getParameters(), context);
|
||||
if (arguments == null) {
|
||||
// Context changed out from under me?
|
||||
return;
|
||||
}
|
||||
method.invoke(arguments).thenAccept(result -> {
|
||||
if (consoleService != null && method.getReturnType() != Void.class) {
|
||||
consoleService.log(null, getDisplay(method) + " returned " + result);
|
||||
}
|
||||
}).exceptionally(ex -> {
|
||||
tool.setStatusInfo(
|
||||
"Invocation of " + getDisplay(method) + " failed: " + ex.getMessage(), true);
|
||||
Msg.error(this, "Invocation of " + method.getPath() + " failed", ex);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@AutoServiceConsumed
|
||||
private DebuggerTraceManagerService traceManager;
|
||||
@AutoServiceConsumed
|
||||
private DebuggerStaticMappingService mappingService;
|
||||
@AutoServiceConsumed
|
||||
private DebuggerConsoleService consoleService;
|
||||
@SuppressWarnings("unused")
|
||||
private final AutoService.Wiring autoServiceWiring;
|
||||
|
||||
public DebuggerMethodActionsPlugin(PluginTool tool) {
|
||||
super(tool);
|
||||
autoServiceWiring = AutoService.wireServicesProvidedAndConsumed(this);
|
||||
tool.addPopupActionProvider(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DockingActionIf> getPopupActions(Tool tool, ActionContext context) {
|
||||
TargetObject curObj = getCurrentTargetObject();
|
||||
if (curObj == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<DockingActionIf> result = new ArrayList<>();
|
||||
PathPredicates matcher = curObj.getModel()
|
||||
.getRootSchema()
|
||||
.matcherForSuitable(TargetMethod.class, curObj.getPath());
|
||||
for (TargetObject obj : matcher.getCachedSuccessors(curObj.getModel().getModelRoot())
|
||||
.values()) {
|
||||
if (!(obj instanceof TargetMethod method)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> arguments = collectArguments(method.getParameters(), context);
|
||||
if (arguments == null) {
|
||||
continue;
|
||||
}
|
||||
result.add(new InvokeMethodAction(method));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private TargetObject getCurrentTargetObject() {
|
||||
if (traceManager == null) {
|
||||
return null;
|
||||
}
|
||||
DebuggerCoordinates current = traceManager.getCurrent();
|
||||
TraceRecorder recorder = current.getRecorder();
|
||||
if (recorder == null) {
|
||||
return null;
|
||||
}
|
||||
TraceObject object = current.getObject();
|
||||
if (object != null) {
|
||||
return recorder.getTargetObject(object);
|
||||
}
|
||||
return recorder.getFocus();
|
||||
}
|
||||
|
||||
private Address dynamicAddress(ProgramLocation loc) {
|
||||
if (loc.getProgram() instanceof TraceProgramView) {
|
||||
return loc.getAddress();
|
||||
}
|
||||
if (traceManager == null) {
|
||||
return null;
|
||||
}
|
||||
ProgramLocation dloc =
|
||||
mappingService.getDynamicLocationFromStatic(traceManager.getCurrentView(), loc);
|
||||
if (dloc == null) {
|
||||
return null;
|
||||
}
|
||||
return dloc.getByteAddress();
|
||||
}
|
||||
|
||||
private Map<String, Object> collectArguments(TargetParameterMap params, ActionContext context) {
|
||||
// The only required non-defaulted argument allowed must be an Address
|
||||
// There must be an Address parameter
|
||||
ParameterDescription<?> addrParam = null;
|
||||
for (ParameterDescription<?> p : params.values()) {
|
||||
if (p.type == Address.class) {
|
||||
if (addrParam != null) {
|
||||
return null;
|
||||
}
|
||||
addrParam = p;
|
||||
}
|
||||
else if (p.required && p.defaultValue == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (addrParam == null) {
|
||||
return null;
|
||||
}
|
||||
if (context instanceof ProgramLocationActionContext ctx) {
|
||||
Address address = dynamicAddress(ctx.getLocation());
|
||||
if (address == null) {
|
||||
return null;
|
||||
}
|
||||
return Map.of(addrParam.name, address);
|
||||
}
|
||||
if (context.getContextObject() instanceof MarkerLocation ml) {
|
||||
Address address = dynamicAddress(new ProgramLocation(ml.getProgram(), ml.getAddr()));
|
||||
if (address == null) {
|
||||
return null;
|
||||
}
|
||||
return Map.of(addrParam.name, address);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.control;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.jdom.JDOMException;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import docking.action.DockingActionIf;
|
||||
import generic.Unique;
|
||||
import ghidra.app.context.ProgramLocationActionContext;
|
||||
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
|
||||
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin;
|
||||
import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingServicePlugin;
|
||||
import ghidra.app.services.DebuggerStaticMappingService;
|
||||
import ghidra.app.services.TraceRecorder;
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.model.*;
|
||||
import ghidra.dbg.target.TargetMethod;
|
||||
import ghidra.dbg.target.TargetMethod.AnnotatedTargetMethod;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.schema.*;
|
||||
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.trace.model.Lifespan;
|
||||
import ghidra.util.database.UndoableTransaction;
|
||||
|
||||
public class DebuggerMethodActionsPluginTest extends AbstractGhidraHeadedDebuggerGUITest {
|
||||
public static final XmlSchemaContext SCHEMA_CTX;
|
||||
public static final TargetObjectSchema MOD_ROOT_SCHEMA;
|
||||
|
||||
static {
|
||||
try {
|
||||
SCHEMA_CTX = XmlSchemaContext.deserialize(
|
||||
EmptyDebuggerObjectModel.class.getResourceAsStream("test_schema.xml"));
|
||||
SchemaBuilder builder =
|
||||
new SchemaBuilder(SCHEMA_CTX, SCHEMA_CTX.getSchema(SCHEMA_CTX.name("Thread")));
|
||||
SchemaName method = SCHEMA_CTX.name("Method");
|
||||
builder.addAttributeSchema(
|
||||
new DefaultAttributeSchema("Advance", method, true, true, true), "manual");
|
||||
builder.addAttributeSchema(
|
||||
new DefaultAttributeSchema("StepExt", method, true, true, true), "manual");
|
||||
builder.addAttributeSchema(
|
||||
new DefaultAttributeSchema("AdvanceWithFlag", method, true, true, true), "manual");
|
||||
builder.addAttributeSchema(
|
||||
new DefaultAttributeSchema("Between", method, true, true, true), "manual");
|
||||
SCHEMA_CTX.replaceSchema(builder.build());
|
||||
MOD_ROOT_SCHEMA = SCHEMA_CTX.getSchema(SCHEMA_CTX.name("Test"));
|
||||
}
|
||||
catch (IOException | JDOMException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
DebuggerListingPlugin listingPlugin;
|
||||
DebuggerStaticMappingService mappingService;
|
||||
DebuggerMethodActionsPlugin methodsPlugin;
|
||||
|
||||
List<String> commands = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
@Before
|
||||
public void setUpMethodAcitonsTest() throws Exception {
|
||||
listingPlugin = addPlugin(tool, DebuggerListingPlugin.class);
|
||||
mappingService = addPlugin(tool, DebuggerStaticMappingServicePlugin.class);
|
||||
methodsPlugin = addPlugin(tool, DebuggerMethodActionsPlugin.class);
|
||||
|
||||
mb = new TestDebuggerModelBuilder() {
|
||||
@Override
|
||||
protected TestDebuggerObjectModel newModel(String typeHint) {
|
||||
commands.clear();
|
||||
return new TestDebuggerObjectModel(typeHint) {
|
||||
@Override
|
||||
public TargetObjectSchema getRootSchema() {
|
||||
return MOD_ROOT_SCHEMA;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TestTargetThread newTestTargetThread(
|
||||
TestTargetThreadContainer container, int tid) {
|
||||
return new TestTargetThread(container, tid) {
|
||||
{
|
||||
changeAttributes(List.of(),
|
||||
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
|
||||
testModel, this),
|
||||
"Initialize");
|
||||
}
|
||||
|
||||
@TargetMethod.Export("Advance")
|
||||
public CompletableFuture<Void> advance(
|
||||
@TargetMethod.Param(
|
||||
description = "The target address",
|
||||
display = "Target",
|
||||
name = "target") Address target) {
|
||||
commands.add("advance(" + target + ")");
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
|
||||
// Takes no address context
|
||||
@TargetMethod.Export("StepExt")
|
||||
public CompletableFuture<Void> stepExt() {
|
||||
commands.add("stepExt");
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
|
||||
// Takes a second required non-default parameter
|
||||
@TargetMethod.Export("AdvanceWithFlag")
|
||||
public CompletableFuture<Void> advanceWithFlag(
|
||||
@TargetMethod.Param(
|
||||
description = "The target address",
|
||||
display = "Target",
|
||||
name = "target") Address address,
|
||||
@TargetMethod.Param(
|
||||
description = "The flag",
|
||||
display = "Flag",
|
||||
name = "flag") boolean flag) {
|
||||
commands.add("advanceWithFlag(" + address + "," + flag + ")");
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
|
||||
// Takes a second address parameter
|
||||
@TargetMethod.Export("Between")
|
||||
public CompletableFuture<Void> between(
|
||||
@TargetMethod.Param(
|
||||
description = "The starting address",
|
||||
display = "Start",
|
||||
name = "start") Address start,
|
||||
@TargetMethod.Param(
|
||||
description = "The ending address",
|
||||
display = "End",
|
||||
name = "end") Address end) {
|
||||
commands.add("between(" + start + "," + end + ")");
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected Collection<TargetMethod> collectMethods(TargetObject object) {
|
||||
return object.getModel()
|
||||
.getRootSchema()
|
||||
.matcherForSuitable(TargetMethod.class, object.getPath())
|
||||
.getCachedSuccessors(object.getModel().getModelRoot())
|
||||
.values()
|
||||
.stream()
|
||||
.filter(o -> o instanceof TargetMethod)
|
||||
.map(o -> (TargetMethod) o)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPopupActionsNoTrace() throws Throwable {
|
||||
createProgram();
|
||||
programManager.openProgram(program);
|
||||
ProgramLocationActionContext ctx =
|
||||
new ProgramLocationActionContext(listingPlugin.getProvider(), program,
|
||||
new ProgramLocation(program, addr(program, 0)), null, null);
|
||||
assertEquals(List.of(), methodsPlugin.getPopupActions(tool, ctx));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPopupActionsNoThread() throws Throwable {
|
||||
createTestModel();
|
||||
recordAndWaitSync();
|
||||
traceManager.openTrace(tb.trace);
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
|
||||
assertEquals(4, collectMethods(mb.testThread1).size());
|
||||
|
||||
createProgramFromTrace(tb.trace);
|
||||
programManager.openProgram(program);
|
||||
ProgramLocationActionContext ctx =
|
||||
new ProgramLocationActionContext(listingPlugin.getProvider(), program,
|
||||
new ProgramLocation(program, addr(program, 0)), null, null);
|
||||
assertEquals(List.of(), methodsPlugin.getPopupActions(tool, ctx));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPopupActions() throws Throwable {
|
||||
createTestModel();
|
||||
TraceRecorder recorder = recordAndWaitSync();
|
||||
traceManager.openTrace(tb.trace);
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
waitOn(recorder.requestFocus(mb.testThread1));
|
||||
waitRecorder(recorder);
|
||||
waitForSwing();
|
||||
|
||||
assertEquals(4, collectMethods(mb.testThread1).size());
|
||||
|
||||
createProgramFromTrace(tb.trace);
|
||||
intoProject(program);
|
||||
|
||||
try (UndoableTransaction tid = UndoableTransaction.start(program, "Add memory")) {
|
||||
program.getMemory()
|
||||
.createInitializedBlock(".text", addr(program, 0x00400000), 0x1000,
|
||||
(byte) 0, monitor, false);
|
||||
}
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
mappingService.addIdentityMapping(tb.trace, program, Lifespan.ALL, true);
|
||||
}
|
||||
waitForDomainObject(tb.trace);
|
||||
waitOn(mappingService.changesSettled());
|
||||
|
||||
programManager.openProgram(program);
|
||||
ProgramLocationActionContext ctx =
|
||||
new ProgramLocationActionContext(listingPlugin.getProvider(), program,
|
||||
new ProgramLocation(program, addr(program, 0x00400000)), null, null);
|
||||
assertEquals(List.of("Advance"),
|
||||
methodsPlugin.getPopupActions(tool, ctx).stream().map(a -> a.getName()).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodInvocation() throws Throwable {
|
||||
createTestModel();
|
||||
TraceRecorder recorder = recordAndWaitSync();
|
||||
traceManager.openTrace(tb.trace);
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
waitOn(recorder.requestFocus(mb.testThread1));
|
||||
waitRecorder(recorder);
|
||||
waitForSwing();
|
||||
|
||||
assertEquals(4, collectMethods(mb.testThread1).size());
|
||||
|
||||
createProgramFromTrace(tb.trace);
|
||||
intoProject(program);
|
||||
|
||||
try (UndoableTransaction tid = UndoableTransaction.start(program, "Add memory")) {
|
||||
program.getMemory()
|
||||
.createInitializedBlock(".text", addr(program, 0x00400000), 0x1000,
|
||||
(byte) 0, monitor, false);
|
||||
}
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
mappingService.addIdentityMapping(tb.trace, program, Lifespan.ALL, true);
|
||||
}
|
||||
waitForDomainObject(tb.trace);
|
||||
waitOn(mappingService.changesSettled());
|
||||
|
||||
programManager.openProgram(program);
|
||||
ProgramLocationActionContext ctx =
|
||||
new ProgramLocationActionContext(listingPlugin.getProvider(), program,
|
||||
new ProgramLocation(program, addr(program, 0x00400000)), null, null);
|
||||
|
||||
DockingActionIf advance = Unique.assertOne(methodsPlugin.getPopupActions(tool, ctx));
|
||||
assertEquals("Advance", advance.getName());
|
||||
performAction(advance, ctx, true);
|
||||
waitRecorder(recorder);
|
||||
|
||||
assertEquals(List.of("advance(00400000)"), commands);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue