GP-1808: Added 'Run to Address'-type actions to right-click menu for some connectors.

This commit is contained in:
Dan 2023-02-07 12:23:16 -05:00
parent 44d7c4f031
commit bde529b4d5
39 changed files with 1663 additions and 136 deletions

View file

@ -120,6 +120,22 @@ public interface DbgThread
*/
CompletableFuture<Void> step(Map<String, ?> args);
/**
* Step (over) the thread until the specified address is reached
*
* @param address the stop address
* @return a future that completes once the thread is running
*/
CompletableFuture<Void> stepToAddress(String address);
/**
* Trace (step into) the thread until the specified address is reached
*
* @param address the stop address
* @return a future that completes once the thread is running
*/
CompletableFuture<Void> traceToAddress(String address);
/**
* Detach from the entire process
*

View file

@ -0,0 +1,73 @@
/* ###
* 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 agent.dbgeng.manager.cmd;
import agent.dbgeng.dbgeng.DebugControl;
import agent.dbgeng.dbgeng.DebugThreadId;
import agent.dbgeng.manager.DbgEvent;
import agent.dbgeng.manager.evt.*;
import agent.dbgeng.manager.impl.DbgManagerImpl;
import agent.dbgeng.manager.impl.DbgThreadImpl;
import ghidra.util.Msg;
public abstract class AbstractDbgExecToAddressCommand extends AbstractDbgCommand<Void> {
private final DebugThreadId id;
private final String address;
public AbstractDbgExecToAddressCommand(DbgManagerImpl manager, DebugThreadId id,
String address) {
super(manager);
this.id = id;
this.address = address;
}
@Override
public boolean handle(DbgEvent<?> evt, DbgPendingCommand<?> pending) {
if (evt instanceof AbstractDbgCompletedCommandEvent && pending.getCommand().equals(this)) {
return evt instanceof DbgCommandErrorEvent ||
!pending.findAllOf(DbgRunningEvent.class).isEmpty();
}
else if (evt instanceof DbgRunningEvent) {
// Event happens no matter which interpreter received the command
pending.claim(evt);
return !pending.findAllOf(AbstractDbgCompletedCommandEvent.class).isEmpty();
}
return false;
}
protected abstract String generateCommand(String address);
@Override
public void invoke() {
String cmd = generateCommand(address);
String prefix = id == null ? "" : "~" + id.id + " ";
DebugControl control = manager.getControl();
DbgThreadImpl eventThread = manager.getEventThread();
if (eventThread != null && eventThread.getId().equals(id)) {
control.execute(cmd);
}
else {
if (manager.isKernelMode()) {
Msg.info(this, "Thread-specific steppign is ignored in kernel-mode");
control.execute(cmd);
}
else {
control.execute(prefix + cmd);
}
}
}
}

View file

@ -0,0 +1,31 @@
/* ###
* 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 agent.dbgeng.manager.cmd;
import agent.dbgeng.dbgeng.DebugThreadId;
import agent.dbgeng.manager.impl.DbgManagerImpl;
public class DbgStepToAddressCommand extends AbstractDbgExecToAddressCommand {
public DbgStepToAddressCommand(DbgManagerImpl manager, DebugThreadId id, String address) {
super(manager, id, address);
}
@Override
protected String generateCommand(String address) {
return "pa " + address;
}
}

View file

@ -0,0 +1,31 @@
/* ###
* 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 agent.dbgeng.manager.cmd;
import agent.dbgeng.dbgeng.DebugThreadId;
import agent.dbgeng.manager.impl.DbgManagerImpl;
public class DbgTraceToAddressCommand extends AbstractDbgExecToAddressCommand {
public DbgTraceToAddressCommand(DbgManagerImpl manager, DebugThreadId id, String address) {
super(manager, id, address);
}
@Override
protected String generateCommand(String address) {
return "ta " + address;
}
}

View file

@ -225,6 +225,20 @@ public class DbgThreadImpl implements DbgThread {
});
}
@Override
public CompletableFuture<Void> stepToAddress(String address) {
return setActive().thenCompose(__ -> {
return manager.execute(new DbgStepToAddressCommand(manager, id, address));
});
}
@Override
public CompletableFuture<Void> traceToAddress(String address) {
return setActive().thenCompose(__ -> {
return manager.execute(new DbgTraceToAddressCommand(manager, id, address));
});
}
@Override
public CompletableFuture<Void> kill() {
return setActive().thenCompose(__ -> {

View file

@ -61,8 +61,6 @@ public interface DbgModelTargetSteppable extends DbgModelTargetObject, TargetSte
switch (kind) {
case SKIP:
throw new UnsupportedOperationException(kind.name());
case ADVANCE: // Why no exec-advance in dbgeng?
return thread.console("advance");
default:
if (this instanceof DbgModelTargetThread) {
DbgModelTargetThread targetThread = (DbgModelTargetThread) this;

View file

@ -23,11 +23,13 @@ import agent.dbgeng.manager.*;
import agent.dbgeng.manager.impl.*;
import agent.dbgeng.model.iface1.*;
import agent.dbgeng.model.impl.DbgModelTargetStackImpl;
import ghidra.dbg.target.TargetThread;
import ghidra.dbg.target.*;
import ghidra.dbg.util.PathUtils;
import ghidra.program.model.address.Address;
public interface DbgModelTargetThread extends //
TargetThread, //
TargetAggregate, //
DbgModelTargetAccessConditioned, //
DbgModelTargetExecutionStateful, //
DbgModelTargetSteppable, //
@ -58,6 +60,24 @@ public interface DbgModelTargetThread extends //
}
}
@TargetMethod.Export("Step to Address (pa)")
public default CompletableFuture<Void> stepToAddress(
@TargetMethod.Param(
description = "The target address",
display = "StopAddress",
name = "address") Address address) {
return getModel().gateFuture(getThread().stepToAddress(address.toString(false)));
}
@TargetMethod.Export("Trace to Address (ta)")
public default CompletableFuture<Void> traceToAddress(
@TargetMethod.Param(
description = "The target address",
display = "StopAddress",
name = "address") Address address) {
return getModel().gateFuture(getThread().traceToAddress(address.toString(false)));
}
@Override
public default CompletableFuture<Void> setActive() {
DbgManagerImpl manager = getManager();

View file

@ -183,8 +183,6 @@ public class DbgModelTargetProcessImpl extends DbgModelTargetObjectImpl
switch (kind) {
case SKIP:
throw new UnsupportedOperationException(kind.name());
case ADVANCE: // Why no exec-advance in dbgeng?
throw new UnsupportedOperationException(kind.name());
default:
return model.gateFuture(process.step(convertToDbg(kind)));
}

View file

@ -15,6 +15,7 @@
*/
package agent.dbgeng.model.impl;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@ -26,6 +27,7 @@ import agent.dbgeng.model.iface1.DbgModelTargetFocusScope;
import agent.dbgeng.model.iface2.*;
import ghidra.dbg.target.TargetEnvironment;
import ghidra.dbg.target.TargetFocusScope;
import ghidra.dbg.target.TargetMethod.AnnotatedTargetMethod;
import ghidra.dbg.target.schema.*;
import ghidra.dbg.util.PathUtils;
@ -50,7 +52,6 @@ public class DbgModelTargetThreadImpl extends DbgModelTargetObjectImpl
implements DbgModelTargetThread {
public static final TargetStepKindSet SUPPORTED_KINDS = TargetStepKindSet.of( //
TargetStepKind.ADVANCE, //
TargetStepKind.FINISH, //
TargetStepKind.LINE, //
TargetStepKind.OVER, //
@ -90,6 +91,9 @@ public class DbgModelTargetThreadImpl extends DbgModelTargetObjectImpl
this.registers = new DbgModelTargetRegisterContainerImpl(this);
this.stack = new DbgModelTargetStackImpl(this, process);
changeAttributes(List.of(), List.of(),
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(), threads.getModel(), this),
"Methods");
changeAttributes(List.of(), List.of( //
registers, //
stack //
@ -145,8 +149,6 @@ public class DbgModelTargetThreadImpl extends DbgModelTargetObjectImpl
switch (kind) {
case SKIP:
throw new UnsupportedOperationException(kind.name());
case ADVANCE: // Why no exec-advance in GDB/MI?
return thread.console("advance");
default:
return model.gateFuture(thread.step(convertToDbg(kind)));
}

View file

@ -15,6 +15,7 @@
*/
package agent.dbgmodel.model.impl;
import java.lang.invoke.MethodHandles;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
@ -37,6 +38,7 @@ import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.target.TargetMethod.AnnotatedTargetMethod;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.util.PathUtils;
import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator;
@ -301,6 +303,8 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
String executionType =
targetThread.getThread().getExecutingProcessorType().description;
attrs.put(TargetEnvironment.ARCH_ATTRIBUTE_NAME, executionType);
attrs.putAll(
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(), model, proxy));
}
if (proxy instanceof TargetRegister) {
DbgModelTargetObject bank = (DbgModelTargetObject) getParent();

View file

@ -359,8 +359,6 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
changeAttributes(List.of(), List.of(), Map.of( //
TargetAccessConditioned.ACCESSIBLE_ATTRIBUTE_NAME, accessible //
), "Accessibility changed");
DbgModelTargetAccessConditioned accessConditioned =
(DbgModelTargetAccessConditioned) proxy;
}
}

View file

@ -342,6 +342,7 @@
</schema>
<schema name="Thread" elementResync="NEVER" attributeResync="NEVER">
<interface name="Thread" />
<interface name="Aggregate" />
<interface name="Access" />
<interface name="ExecutionStateful" />
<interface name="Steppable" />
@ -364,8 +365,18 @@
<attribute name="Id" schema="OBJECT" />
<attribute name="Name" schema="OBJECT" />
<attribute name="_arch" schema="STRING" />
<attribute name="Step to Address (pa)" schema="Method" />
<attribute name="Trace to Address (ta)" schema="Method" />
<attribute schema="ANY" />
</schema>
<schema name="Method" elementResync="NEVER" attributeResync="NEVER">
<interface name="Method" />
<element schema="VOID" />
<attribute name="_display" schema="STRING" fixed="yes" hidden="yes" />
<attribute name="_return_type" schema="TYPE" fixed="yes" hidden="yes" />
<attribute name="_parameters" schema="MAP_PARAMETERS" fixed="yes" hidden="yes" />
<attribute schema="VOID" />
</schema>
<schema name="Module" elementResync="NEVER" attributeResync="NEVER">
<interface name="Module" />
<element schema="VOID" />

View file

@ -123,6 +123,30 @@ public interface GdbThread
*/
CompletableFuture<Void> step(StepCmd suffix);
/**
* Advance the thread to the given location
*
* <p>
* This is equivalent to the CLI command {@code advance}.
*
* <p>
* Note that the command can complete before the thread has finished advancing. The command
* completes as soon as the thread is running. A separate stop event is emitted when the thread
* has stopped.
*
* @param loc the location to stop at, same syntax as breakpoint locations
* @return a future that completes once the thread is running
*/
CompletableFuture<Void> advance(String loc);
/**
* Advance the thread to the given address
*
* @param addr the address
* @see #advance(String)
*/
CompletableFuture<Void> advance(long addr);
/**
* Detach from the entire process
*

View file

@ -45,6 +45,7 @@ public interface GdbBreakpointInsertions {
/**
* Insert a breakpoint (usually a watchpoint) at the given address range
*
* <p>
* Note, this implements the length by casting the address pointer to a
* fixed-length-char-array-pointer where the array has the given length. Support for specific
* lengths may vary by platform.

View file

@ -286,6 +286,17 @@ public class GdbThreadImpl implements GdbThread {
return execute(new GdbStepCommand(manager, id, suffix));
}
@Override
public CompletableFuture<Void> advance(String loc) {
// There's no exec-advance or similar in MI2....
return console("advance " + loc, CompletesWithRunning.MUST);
}
@Override
public CompletableFuture<Void> advance(long addr) {
return advance(String.format("*0x%x", addr));
}
@Override
public CompletableFuture<Void> kill() {
return execute(new GdbKillCommand(manager, id));

View file

@ -192,9 +192,6 @@ public class GdbModelTargetInferior
case SKIP:
case EXTENDED:
throw new UnsupportedOperationException(kind.name());
case ADVANCE: // Why no exec-advance in GDB/MI?
// TODO: This doesn't work, since advance requires a parameter
return model.gateFuture(inferior.console("advance", CompletesWithRunning.MUST));
default:
return model.gateFuture(inferior.step(GdbModelTargetThread.convertToGdb(kind)));
}

View file

@ -15,6 +15,7 @@
*/
package agent.gdb.model.impl;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@ -24,14 +25,15 @@ import agent.gdb.manager.GdbManager.StepCmd;
import agent.gdb.manager.impl.GdbFrameInfo;
import agent.gdb.manager.impl.GdbThreadInfo;
import agent.gdb.manager.impl.cmd.GdbStateChangeRecord;
import agent.gdb.manager.impl.cmd.GdbConsoleExecCommand.CompletesWithRunning;
import agent.gdb.manager.reason.GdbBreakpointHitReason;
import ghidra.async.AsyncUtils;
import ghidra.dbg.agent.DefaultTargetObject;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetMethod.AnnotatedTargetMethod;
import ghidra.dbg.target.schema.*;
import ghidra.dbg.util.PathUtils;
import ghidra.lifecycle.Internal;
import ghidra.program.model.address.Address;
import ghidra.util.Msg;
@TargetObjectSchemaInfo(
@ -44,15 +46,13 @@ public class GdbModelTargetThread
extends DefaultTargetObject<TargetObject, GdbModelTargetThreadContainer> implements
TargetThread, TargetExecutionStateful, TargetSteppable, TargetAggregate,
GdbModelSelectableObject {
protected static final TargetStepKindSet SUPPORTED_KINDS = TargetStepKindSet.of( //
TargetStepKind.ADVANCE, //
TargetStepKind.FINISH, //
TargetStepKind.LINE, //
TargetStepKind.OVER, //
TargetStepKind.OVER_LINE, //
TargetStepKind.RETURN, //
TargetStepKind.UNTIL, //
TargetStepKind.EXTENDED);
protected static final TargetStepKindSet SUPPORTED_KINDS = TargetStepKindSet.of(
TargetStepKind.FINISH,
TargetStepKind.LINE,
TargetStepKind.OVER,
TargetStepKind.OVER_LINE,
TargetStepKind.RETURN,
TargetStepKind.UNTIL);
protected static String indexThread(int threadId) {
return PathUtils.makeIndex(threadId);
@ -87,12 +87,15 @@ public class GdbModelTargetThread
this.stack = new GdbModelTargetStack(this, inferior);
changeAttributes(List.of(), List.of(stack), Map.of( //
STATE_ATTRIBUTE_NAME, state = convertState(thread.getState()), //
SUPPORTED_STEP_KINDS_ATTRIBUTE_NAME, SUPPORTED_KINDS, //
SHORT_DISPLAY_ATTRIBUTE_NAME, shortDisplay = computeShortDisplay(), //
DISPLAY_ATTRIBUTE_NAME, display = computeDisplay() //
), "Initialized");
changeAttributes(List.of(), List.of(),
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(), impl, this),
"Methods");
changeAttributes(List.of(), List.of(stack), Map.of(
STATE_ATTRIBUTE_NAME, state = convertState(thread.getState()),
SUPPORTED_STEP_KINDS_ATTRIBUTE_NAME, SUPPORTED_KINDS,
SHORT_DISPLAY_ATTRIBUTE_NAME, shortDisplay = computeShortDisplay(),
DISPLAY_ATTRIBUTE_NAME, display = computeDisplay()),
"Initialized");
updateInfo().exceptionally(ex -> {
Msg.error(this, "Could not initialize thread info");
@ -214,14 +217,20 @@ public class GdbModelTargetThread
case SKIP:
case EXTENDED:
throw new UnsupportedOperationException(kind.name());
case ADVANCE: // Why no exec-advance in GDB/MI?
// TODO: This doesn't work, since advance requires a parameter
return model.gateFuture(thread.console("advance", CompletesWithRunning.CANNOT));
default:
return model.gateFuture(thread.step(convertToGdb(kind)));
}
}
@TargetMethod.Export("Advance")
public CompletableFuture<Void> advance(
@TargetMethod.Param(
name = "target",
display = "Target",
description = "The address to advance to") Address target) {
return impl.gateFuture(thread.advance(target.getOffset()));
}
protected void invalidateRegisterCaches() {
stack.invalidateRegisterCaches();
}
@ -257,5 +266,4 @@ public class GdbModelTargetThread
this.base = (Integer) value;
updateInfo();
}
}

View file

@ -0,0 +1,64 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.lldb.manager.cmd;
import java.math.BigInteger;
import SWIG.*;
import agent.lldb.manager.LldbEvent;
import agent.lldb.manager.evt.*;
import agent.lldb.manager.impl.LldbManagerImpl;
import ghidra.util.Msg;
public class LldbRunToAddressCommand extends AbstractLldbCommand<Void> {
private SBThread thread;
private final BigInteger addr;
public LldbRunToAddressCommand(LldbManagerImpl manager, SBThread thread, BigInteger addr) {
super(manager);
this.thread = thread;
this.addr = addr;
}
@Override
public boolean handle(LldbEvent<?> evt, LldbPendingCommand<?> pending) {
if (evt instanceof AbstractLldbCompletedCommandEvent && pending.getCommand().equals(this)) {
return evt instanceof LldbCommandErrorEvent ||
!pending.findAllOf(LldbRunningEvent.class).isEmpty();
}
else if (evt instanceof LldbRunningEvent) {
// Event happens no matter which interpreter received the command
pending.claim(evt);
return !pending.findAllOf(AbstractLldbCompletedCommandEvent.class).isEmpty();
}
return false;
}
@Override
public void invoke() {
if (thread == null || !thread.IsValid()) {
thread = manager.getCurrentThread();
}
SBError error = new SBError();
thread.RunToAddress(addr, error);
if (!error.Success()) {
SBStream stream = new SBStream();
error.GetDescription(stream);
Msg.error(this, error.GetType() + " while running to address: " + stream.GetData());
}
}
}

View file

@ -86,11 +86,6 @@ public class LldbStepCommand extends AbstractLldbCommand<Void> {
case FINISH:
thread.StepOutOfFrame(thread.GetSelectedFrame(), error);
break;
case ADVANCE:
SBFileSpec file = (SBFileSpec) args.get("File");
long line = (long) args.get("Line");
error = thread.StepOverUntil(thread.GetSelectedFrame(), file, line);
break;
case EXTENDED:
manager.execute(new LldbEvaluateCommand(manager, lastCommand));
break;

View file

@ -15,6 +15,7 @@
*/
package agent.lldb.model.impl;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@ -24,13 +25,16 @@ import agent.lldb.lldb.DebugClient;
import agent.lldb.manager.LldbCause;
import agent.lldb.manager.LldbReason;
import agent.lldb.manager.LldbReason.Reasons;
import agent.lldb.manager.cmd.LldbRunToAddressCommand;
import agent.lldb.manager.cmd.LldbStepCommand;
import agent.lldb.model.iface1.LldbModelTargetFocusScope;
import agent.lldb.model.iface2.*;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetMethod.AnnotatedTargetMethod;
import ghidra.dbg.target.schema.*;
import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode;
import ghidra.dbg.util.PathUtils;
import ghidra.program.model.address.Address;
@TargetObjectSchemaInfo(
name = "Thread",
@ -49,7 +53,6 @@ public class LldbModelTargetThreadImpl extends LldbModelTargetObjectImpl
implements LldbModelTargetThread {
public static final TargetStepKindSet SUPPORTED_KINDS = TargetStepKindSet.of( //
TargetStepKind.ADVANCE, //
TargetStepKind.FINISH, //
TargetStepKind.LINE, //
TargetStepKind.OVER, //
@ -82,6 +85,9 @@ public class LldbModelTargetThreadImpl extends LldbModelTargetObjectImpl
this.stack = new LldbModelTargetStackImpl(this, process);
changeAttributes(List.of(), List.of(),
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(), threads.getModel(), this),
"Methods");
changeAttributes(List.of(), List.of( //
stack //
), Map.of( //
@ -102,6 +108,7 @@ public class LldbModelTargetThreadImpl extends LldbModelTargetObjectImpl
getModel().addModelObject(modelObject, this);
}
@Override
public String getDescription(int level) {
SBStream stream = new SBStream();
SBThread thread = (SBThread) getModelObject();
@ -114,8 +121,9 @@ public class LldbModelTargetThreadImpl extends LldbModelTargetObjectImpl
String tidstr = DebugClient.getId(getThread());
if (base == 16) {
tidstr = "0x" + tidstr;
} else {
tidstr = Long.toString(Long.parseLong(tidstr,16));
}
else {
tidstr = Long.toString(Long.parseLong(tidstr, 16));
}
return "[" + tidstr + "]";
}
@ -138,12 +146,25 @@ public class LldbModelTargetThreadImpl extends LldbModelTargetObjectImpl
@Override
public CompletableFuture<Void> step(TargetStepKind kind) {
return getModel().gateFuture(getManager().execute(new LldbStepCommand(getManager(), getThread(), kind, null)));
return getModel().gateFuture(
getManager().execute(new LldbStepCommand(getManager(), getThread(), kind, null)));
}
@Override
public CompletableFuture<Void> step(Map<String, ?> args) {
return getModel().gateFuture(getManager().execute(new LldbStepCommand(getManager(), getThread(), null, args)));
return getModel().gateFuture(
getManager().execute(new LldbStepCommand(getManager(), getThread(), null, args)));
}
@TargetMethod.Export("Run to Address")
public CompletableFuture<Void> runToAddress(
@TargetMethod.Param(
description = "The target address",
display = "Address",
name = "address") Address address) {
return getModel().gateFuture(
getManager().execute(new LldbRunToAddressCommand(getManager(), getThread(),
address.getOffsetAsBigInteger())));
}
@Override

View file

@ -137,8 +137,6 @@ public enum GadpValueUtils {
public static TargetStepKind getStepKind(Gadp.StepKind kind) {
switch (kind) {
case SK_ADVANCE:
return TargetStepKind.ADVANCE;
case SK_FINISH:
return TargetStepKind.FINISH;
case SK_INTO:
@ -164,8 +162,6 @@ public enum GadpValueUtils {
public static Gadp.StepKind makeStepKind(TargetStepKind kind) {
switch (kind) {
case ADVANCE:
return Gadp.StepKind.SK_ADVANCE;
case FINISH:
return Gadp.StepKind.SK_FINISH;
case INTO:

View file

@ -90,7 +90,6 @@ message BreakKindsSet {
enum StepKind {
SK_INTO = 0;
SK_ADVANCE = 1;
SK_FINISH = 2;
SK_LINE = 3;
SK_OVER = 4;

View file

@ -55,7 +55,6 @@ public class JdiModelTargetThread extends JdiModelTargetObjectReference implemen
JdiModelSelectableObject {
protected static final TargetStepKindSet SUPPORTED_KINDS = TargetStepKindSet.of( //
TargetStepKind.ADVANCE, //
TargetStepKind.FINISH, //
TargetStepKind.LINE, //
TargetStepKind.OVER, //
@ -346,7 +345,6 @@ public class JdiModelTargetThread extends JdiModelTargetObjectReference implemen
depth = StepRequest.STEP_LINE;
break;
case FINISH:
case ADVANCE:
depth = StepRequest.STEP_OUT;
break;
case SKIP:

View file

@ -612,9 +612,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
public DebuggerControlPlugin(PluginTool tool) {
super(tool);
tool.addContextListener(this);
createActions();
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -15,16 +15,30 @@
*/
package ghidra.dbg.target;
import java.lang.annotation.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.reflect.TypeUtils;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
import ghidra.dbg.agent.DefaultTargetObject;
import ghidra.dbg.error.DebuggerIllegalArgumentException;
import ghidra.dbg.target.TargetMethod.*;
import ghidra.dbg.target.TargetMethod.TargetParameterMap.EmptyTargetParameterMap;
import ghidra.dbg.target.TargetMethod.TargetParameterMap.ImmutableTargetParameterMap;
import ghidra.dbg.target.schema.TargetAttributeType;
import ghidra.dbg.util.CollectionUtils.AbstractEmptyMap;
import ghidra.dbg.util.CollectionUtils.AbstractNMap;
import utilities.util.reflection.ReflectionUtilities;
/**
* An object which can be invoked as a method
@ -38,11 +52,207 @@ public interface TargetMethod extends TargetObject {
String RETURN_TYPE_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "return_type";
public static String REDIRECT = "<=";
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Export {
String value();
}
interface Value<T> {
boolean specified();
T value();
}
@interface BoolValue {
boolean specified() default true;
boolean value();
record Val(BoolValue v) implements Value<Boolean> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public Boolean value() {
return v.value();
}
}
}
@interface IntValue {
boolean specified() default true;
int value();
record Val(IntValue v) implements Value<Integer> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public Integer value() {
return v.value();
}
}
}
@interface LongValue {
boolean specified() default true;
long value();
record Val(LongValue v) implements Value<Long> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public Long value() {
return v.value();
}
}
}
@interface FloatValue {
boolean specified() default true;
float value();
record Val(FloatValue v) implements Value<Float> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public Float value() {
return v.value();
}
}
}
@interface DoubleValue {
boolean specified() default true;
double value();
record Val(DoubleValue v) implements Value<Double> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public Double value() {
return v.value();
}
}
}
@interface BytesValue {
boolean specified() default true;
byte[] value();
record Val(BytesValue v) implements Value<byte[]> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public byte[] value() {
return v.value();
}
}
}
@interface StringValue {
boolean specified() default true;
String value();
record Val(StringValue v) implements Value<String> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public String value() {
return v.value();
}
}
}
@interface StringsValue {
boolean specified() default true;
String[] value();
record Val(StringsValue v) implements Value<List<String>> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public List<String> value() {
return List.of(v.value());
}
}
}
// TODO: Address, Range, BreakKind[Set], etc?
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface Param {
List<Function<Param, Value<?>>> DEFAULTS = List.of(
p -> new BoolValue.Val(p.defaultBool()),
p -> new IntValue.Val(p.defaultInt()),
p -> new LongValue.Val(p.defaultLong()),
p -> new FloatValue.Val(p.defaultFloat()),
p -> new DoubleValue.Val(p.defaultDouble()),
p -> new BytesValue.Val(p.defaultBytes()),
p -> new StringValue.Val(p.defaultString()));
String name();
String display();
String description();
// TODO: Something that hints at changes in activation?
boolean required() default true;
BoolValue defaultBool() default @BoolValue(specified = false, value = false);
IntValue defaultInt() default @IntValue(specified = false, value = 0);
LongValue defaultLong() default @LongValue(specified = false, value = 0);
FloatValue defaultFloat() default @FloatValue(specified = false, value = 0);
DoubleValue defaultDouble() default @DoubleValue(specified = false, value = 0);
BytesValue defaultBytes() default @BytesValue(specified = false, value = {});
StringValue defaultString() default @StringValue(specified = false, value = "");
StringsValue choicesString() default @StringsValue(specified = false, value = {});
}
/**
* A description of a method parameter
*
* <p>
* TODO: For convenience, these should be programmable via annotations.
* <P>
* TODO: Should this be incorporated into schemas?
*
@ -85,6 +295,83 @@ public interface TargetMethod extends TargetObject {
choices);
}
protected static boolean isRequired(Class<?> type, Param param) {
if (!type.isPrimitive()) {
return param.required();
}
if (type == boolean.class) {
return !param.defaultBool().specified();
}
if (type == int.class) {
return !param.defaultInt().specified();
}
if (type == long.class) {
return !param.defaultLong().specified();
}
if (type == float.class) {
return !param.defaultFloat().specified();
}
if (type == double.class) {
return !param.defaultDouble().specified();
}
throw new IllegalArgumentException("Parameter type not allowed: " + type);
}
protected static Object getDefault(Param annot) {
List<Object> defaults = new ArrayList<>();
for (Function<Param, Value<?>> df : Param.DEFAULTS) {
Value<?> value = df.apply(annot);
if (value.specified()) {
defaults.add(value.value());
}
}
if (defaults.isEmpty()) {
return null;
}
if (defaults.size() > 1) {
throw new IllegalArgumentException(
"Can only specify one default value. Got " + defaults);
}
return defaults.get(0);
}
protected static <T> T getDefault(Class<T> type, Param annot) {
Object dv = getDefault(annot);
if (dv == null) {
return null;
}
if (!type.isInstance(dv)) {
throw new IllegalArgumentException(
"Type of default does not match that of parameter. Expected type " + type +
". Got (" + dv.getClass() + ")" + dv);
}
return type.cast(dv);
}
protected static <T> ParameterDescription<T> annotated(Class<T> type, Param annot) {
boolean required = isRequired(type, annot);
T defaultValue = getDefault(type, annot);
return ParameterDescription.create(type, annot.name(),
required, defaultValue, annot.display(), annot.description());
}
public static ParameterDescription<?> annotated(Parameter parameter) {
Param annot = parameter.getAnnotation(Param.class);
if (annot == null) {
throw new IllegalArgumentException(
"Missing @" + Param.class.getSimpleName() + " on " + parameter);
}
if (annot.choicesString().specified()) {
if (parameter.getType() != String.class) {
throw new IllegalArgumentException(
"Can only specify choices for String parameter");
}
return ParameterDescription.choices(String.class, annot.name(),
List.of(annot.choicesString().value()), annot.display(), annot.description());
}
return annotated(MethodType.methodType(parameter.getType()).wrap().returnType(), annot);
}
public final Class<T> type;
public final String name;
public final T defaultValue;
@ -199,13 +486,78 @@ public interface TargetMethod extends TargetObject {
public static TargetParameterMap ofEntries(
Entry<String, ParameterDescription<?>>... entries) {
Map<String, ParameterDescription<?>> ordered = new LinkedHashMap<>();
for (Entry<String, ParameterDescription<?>> ent: entries) {
for (Entry<String, ParameterDescription<?>> ent : entries) {
ordered.put(ent.getKey(), ent.getValue());
}
return new ImmutableTargetParameterMap(ordered);
}
}
class AnnotatedTargetMethod extends DefaultTargetObject<TargetObject, TargetObject>
implements TargetMethod {
public static Map<String, AnnotatedTargetMethod> collectExports(Lookup lookup,
AbstractDebuggerObjectModel model, TargetObject parent) {
Map<String, AnnotatedTargetMethod> result = new HashMap<>();
Set<Class<?>> allClasses = new LinkedHashSet<>();
allClasses.add(parent.getClass());
allClasses.addAll(ReflectionUtilities.getAllParents(parent.getClass()));
for (Class<?> declCls : allClasses) {
for (Method method : declCls.getDeclaredMethods()) {
Export annot = method.getAnnotation(Export.class);
if (annot == null || result.containsKey(annot.value())) {
continue;
}
result.put(annot.value(),
new AnnotatedTargetMethod(lookup, model, parent, method, annot));
}
}
return result;
}
private final MethodHandle handle;
private final TargetParameterMap params;
public AnnotatedTargetMethod(Lookup lookup, AbstractDebuggerObjectModel model,
TargetObject parent, Method method, Export annot) {
super(model, parent, annot.value(), "Method");
try {
this.handle = lookup.unreflect(method).bindTo(parent);
}
catch (IllegalAccessException e) {
throw new IllegalArgumentException("Method " + method + " is not accessible");
}
this.params = TargetMethod.makeParameters(
Stream.of(method.getParameters()).map(ParameterDescription::annotated));
Map<TypeVariable<?>, Type> argsCf = TypeUtils
.getTypeArguments(method.getGenericReturnType(), CompletableFuture.class);
Type typeCfT = argsCf.get(CompletableFuture.class.getTypeParameters()[0]);
Class<?> returnType = TypeUtils.getRawType(typeCfT, typeCfT);
changeAttributes(List.of(), Map.ofEntries(
Map.entry(RETURN_TYPE_ATTRIBUTE_NAME, returnType),
Map.entry(PARAMETERS_ATTRIBUTE_NAME, params)),
"Initialize");
}
@SuppressWarnings("unchecked")
@Override
public CompletableFuture<Object> invoke(Map<String, ?> arguments) {
Map<String, ?> valid = TargetMethod.validateArguments(params, arguments, false);
List<Object> args = new ArrayList<>(params.size());
for (ParameterDescription<?> p : params.values()) {
args.add(p.get(valid));
}
try {
return (CompletableFuture<Object>) handle.invokeWithArguments(args);
}
catch (Throwable e) {
return CompletableFuture.failedFuture(e);
}
}
}
/**
* Construct a map of parameter descriptions from a stream
*

View file

@ -66,31 +66,12 @@ public interface TargetSteppable extends TargetObject {
}
enum TargetStepKind {
/**
* Step strictly forward
*
* <p>
* To avoid runaway execution, stepping should cease if execution returns from the current
* frame.
*
* <p>
* In more detail: step until execution reaches the instruction following this one,
* regardless of the current frame. This differs from {@link #UNTIL} in that it doesn't
* regard the current frame.
*/
ADVANCE,
/**
* Step out of the current function.
*
* <p>
* In more detail: step until the object has executed the return instruction that returns
* from the current frame.
*
* <p>
* TODO: This step is geared toward GDB's {@code advance}, which actually takes a parameter.
* Perhaps this API should adjust to accommodate stepping parameters. Would probably want a
* strict set of forms, though, and a given kind should have the same form everywhere. If we
* do that, then we could do nifty pop-up actions, like "Step: Advance to here".
*/
FINISH,
/**

View file

@ -25,6 +25,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.TypeUtils;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.target.TargetMethod;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema.MinimalSchemaContext;
@ -198,6 +199,51 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext {
}
}
protected void addExportedTargetMethodsFromClass(SchemaBuilder builder,
Class<? extends TargetObject> declCls, Class<? extends TargetObject> cls) {
for (Method declMethod : declCls.getDeclaredMethods()) {
int mod = declMethod.getModifiers();
if (!Modifier.isPublic(mod)) {
continue;
}
TargetMethod.Export export = declMethod.getAnnotation(TargetMethod.Export.class);
if (export == null) {
continue;
}
AttributeSchema exists = builder.getAttributeSchema(export.value());
if (exists != null) {
continue;
}
SchemaName snMethod = new SchemaName("Method");
if (getSchemaOrNull(snMethod) == null) {
builder(snMethod)
.addInterface(TargetMethod.class)
.setDefaultElementSchema(EnumerableTargetObjectSchema.VOID.getName())
.addAttributeSchema(
new DefaultAttributeSchema(TargetObject.DISPLAY_ATTRIBUTE_NAME,
EnumerableTargetObjectSchema.STRING.getName(), true, true, true),
"default")
.addAttributeSchema(
new DefaultAttributeSchema(TargetMethod.RETURN_TYPE_ATTRIBUTE_NAME,
EnumerableTargetObjectSchema.TYPE.getName(), true, true, true),
"default")
.addAttributeSchema(
new DefaultAttributeSchema(TargetMethod.PARAMETERS_ATTRIBUTE_NAME,
EnumerableTargetObjectSchema.MAP_PARAMETERS.getName(), true, true,
true),
"default")
.setDefaultAttributeSchema(AttributeSchema.DEFAULT_VOID)
.buildAndAdd();
}
builder.addAttributeSchema(
new DefaultAttributeSchema(export.value(), snMethod, true, true, true), declMethod);
}
}
protected TargetObjectSchema fromAnnotatedClass(Class<? extends TargetObject> cls) {
synchronized (namesByClass) {
SchemaName name = nameFromAnnotatedClass(cls);
@ -268,27 +314,24 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext {
throw new IllegalArgumentException(
"Could not identify unique element class (" + bounds + ") for " + cls);
}
else {
Class<? extends TargetObject> bound = bounds.iterator().next();
SchemaName schemaName;
try {
schemaName = nameFromClass(bound);
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Could not get schema name from bound " + bound + " of " + cls +
".fetchElements()",
e);
}
builder.setDefaultElementSchema(schemaName);
Class<? extends TargetObject> bound = bounds.iterator().next();
SchemaName schemaName;
try {
schemaName = nameFromClass(bound);
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Could not get schema name from bound " + bound + " of " + cls +
".fetchElements()",
e);
}
builder.setDefaultElementSchema(schemaName);
}
addPublicMethodsFromClass(builder, cls, cls);
for (Class<?> parent : allParents) {
if (TargetObject.class.isAssignableFrom(parent)) {
addPublicMethodsFromClass(builder, parent.asSubclass(TargetObject.class),
cls);
addPublicMethodsFromClass(builder, parent.asSubclass(TargetObject.class), cls);
}
}
for (TargetAttributeType at : info.attributes()) {
@ -299,6 +342,13 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext {
}
builder.replaceAttributeSchema(attrSchema, at);
}
addExportedTargetMethodsFromClass(builder, cls, cls);
for (Class<?> parent : allParents) {
if (TargetObject.class.isAssignableFrom(parent)) {
addExportedTargetMethodsFromClass(builder, parent.asSubclass(TargetObject.class),
cls);
}
}
return builder;
}

View file

@ -39,6 +39,10 @@ public class DefaultSchemaContext implements SchemaContext {
schemas.put(schema.getName(), schema);
}
public synchronized void replaceSchema(TargetObjectSchema schema) {
schemas.put(schema.getName(), schema);
}
@Override
public synchronized TargetObjectSchema getSchemaOrNull(SchemaName name) {
return schemas.get(name);

View file

@ -24,7 +24,7 @@ public class DefaultTargetObjectSchema
implements TargetObjectSchema, Comparable<DefaultTargetObjectSchema> {
private static final String INDENT = " ";
protected static class DefaultAttributeSchema
public static class DefaultAttributeSchema
implements AttributeSchema, Comparable<DefaultAttributeSchema> {
private final String name;
private final SchemaName schema;

View file

@ -36,7 +36,7 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
* <p>
* The described value can be any primitive or a {@link TargetObject}.
*/
ANY("ANY", Object.class) {
ANY(Object.class) {
@Override
public SchemaName getDefaultElementSchema() {
return OBJECT.getName();
@ -53,7 +53,7 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
* <p>
* This requires nothing more than the described value to be a {@link TargetObject}.
*/
OBJECT("OBJECT", TargetObject.class) {
OBJECT(TargetObject.class) {
@Override
public SchemaName getDefaultElementSchema() {
return OBJECT.getName();
@ -64,6 +64,7 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
return AttributeSchema.DEFAULT_ANY;
}
},
TYPE(Class.class),
/**
* A type so restrictive nothing can satisfy it.
*
@ -72,22 +73,23 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
* the default attribute when only certain enumerated attributes are allowed. It is also used as
* the type for the children of primitives, since primitives cannot have successors.
*/
VOID("VOID", Void.class, void.class),
BOOL("BOOL", Boolean.class, boolean.class),
BYTE("BYTE", Byte.class, byte.class),
SHORT("SHORT", Short.class, short.class),
INT("INT", Integer.class, int.class),
LONG("LONG", Long.class, long.class),
STRING("STRING", String.class),
ADDRESS("ADDRESS", Address.class),
RANGE("RANGE", AddressRange.class),
DATA_TYPE("DATA_TYPE", TargetDataType.class),
LIST_OBJECT("LIST_OBJECT", TargetObjectList.class),
MAP_PARAMETERS("MAP_PARAMETERS", TargetParameterMap.class),
SET_ATTACH_KIND("SET_ATTACH_KIND", TargetAttachKindSet.class), // TODO: Limited built-in generics
SET_BREAKPOINT_KIND("SET_BREAKPOINT_KIND", TargetBreakpointKindSet.class),
SET_STEP_KIND("SET_STEP_KIND", TargetStepKindSet.class),
EXECUTION_STATE("EXECUTION_STATE", TargetExecutionState.class);
VOID(Void.class, void.class),
BOOL(Boolean.class, boolean.class),
BYTE(Byte.class, byte.class),
SHORT(Short.class, short.class),
INT(Integer.class, int.class),
LONG(Long.class, long.class),
STRING(String.class),
ADDRESS(Address.class),
RANGE(AddressRange.class),
DATA_TYPE(TargetDataType.class),
// TODO: Limited built-in generics?
LIST_OBJECT(TargetObjectList.class),
MAP_PARAMETERS(TargetParameterMap.class),
SET_ATTACH_KIND(TargetAttachKindSet.class),
SET_BREAKPOINT_KIND(TargetBreakpointKindSet.class),
SET_STEP_KIND(TargetStepKindSet.class),
EXECUTION_STATE(TargetExecutionState.class);
public static final class MinimalSchemaContext extends DefaultSchemaContext {
public static final SchemaContext INSTANCE = new MinimalSchemaContext();
@ -126,8 +128,8 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
private final SchemaName name;
private final List<Class<?>> types;
private EnumerableTargetObjectSchema(String name, Class<?>... types) {
this.name = new SchemaName(name);
private EnumerableTargetObjectSchema(Class<?>... types) {
this.name = new SchemaName(this.name());
this.types = List.of(types);
}

View file

@ -44,6 +44,21 @@ public class SchemaBuilder {
this.name = name;
}
public SchemaBuilder(DefaultSchemaContext context, TargetObjectSchema schema) {
this(context, schema.getName());
setType(schema.getType());
setInterfaces(schema.getInterfaces());
setCanonicalContainer(schema.isCanonicalContainer());
elementSchemas.putAll(schema.getElementSchemas());
setDefaultElementSchema(schema.getDefaultElementSchema());
setElementResyncMode(schema.getElementResyncMode());
attributeSchemas.putAll(schema.getAttributeSchemas());
setDefaultAttributeSchema(schema.getDefaultAttributeSchema());
setAttributeResyncMode(schema.getAttributeResyncMode());
}
public SchemaBuilder setType(Class<?> type) {
this.type = type;
return this;

View file

@ -478,7 +478,8 @@ public interface TargetObjectSchema {
throw new IllegalArgumentException("Must provide a specific interface");
}
PathMatcher result = new PathMatcher();
Private.searchFor(this, result, prefix, true, type, requireCanonical, new HashSet<>());
Private.searchFor(this, result, prefix, true, type, false, requireCanonical,
new HashSet<>());
return result;
}
@ -610,37 +611,44 @@ public interface TargetObjectSchema {
private static void searchFor(TargetObjectSchema sch, PathMatcher result,
List<String> prefix, boolean parentIsCanonical, Class<? extends TargetObject> type,
boolean requireCanonical, Set<TargetObjectSchema> visited) {
boolean requireAggregate, boolean requireCanonical,
Set<TargetObjectSchema> visited) {
if (sch instanceof EnumerableTargetObjectSchema) {
return;
}
if (sch.getInterfaces().contains(type) && (parentIsCanonical || !requireCanonical)) {
result.addPattern(prefix);
}
if (!visited.add(sch)) {
return;
}
if (sch.getInterfaces().contains(type) && parentIsCanonical) {
result.addPattern(prefix);
if (requireAggregate && !sch.getInterfaces().contains(TargetAggregate.class)) {
return;
}
SchemaContext ctx = sch.getContext();
boolean isCanonical = sch.isCanonicalContainer();
for (Entry<String, SchemaName> ent : sch.getElementSchemas().entrySet()) {
List<String> extended = PathUtils.index(prefix, ent.getKey());
TargetObjectSchema elemSchema = ctx.getSchema(ent.getValue());
searchFor(elemSchema, result, extended, isCanonical, type, requireCanonical,
visited);
searchFor(elemSchema, result, extended, isCanonical, type, requireAggregate,
requireCanonical, visited);
}
List<String> deExtended = PathUtils.extend(prefix, "[]");
TargetObjectSchema deSchema = ctx.getSchema(sch.getDefaultElementSchema());
searchFor(deSchema, result, deExtended, isCanonical, type, requireCanonical, visited);
searchFor(deSchema, result, deExtended, isCanonical, type, requireAggregate,
requireCanonical, visited);
for (Entry<String, AttributeSchema> ent : sch.getAttributeSchemas().entrySet()) {
List<String> extended = PathUtils.extend(prefix, ent.getKey());
TargetObjectSchema attrSchema = ctx.getSchema(ent.getValue().getSchema());
searchFor(attrSchema, result, extended, parentIsCanonical, type, requireCanonical,
visited);
searchFor(attrSchema, result, extended, parentIsCanonical, type, requireAggregate,
requireCanonical, visited);
}
List<String> daExtended = PathUtils.extend(prefix, "");
TargetObjectSchema daSchema =
ctx.getSchema(sch.getDefaultAttributeSchema().getSchema());
searchFor(daSchema, result, daExtended, parentIsCanonical, type, requireCanonical,
visited);
searchFor(daSchema, result, daExtended, parentIsCanonical, type, requireAggregate,
requireCanonical, visited);
visited.remove(sch);
}
@ -774,6 +782,34 @@ public interface TargetObjectSchema {
return null;
}
/**
* Search for all suitable objects with this schema at the given path
*
* <p>
* This behaves like {@link #searchForSuitable(Class, List)}, except that it returns a matcher
* for all possibilities. Conventionally, when the client uses the matcher to find suitable
* objects and must choose from among the results, those having the longer paths should be
* preferred. More specifically, it should prefer those sharing the longer path prefixes with
* the given path. The client should <em>not</em> just take the first objects, since these will
* likely have the shortest paths. If exactly one object is required, consider using
* {@link #searchForSuitable(Class, List)} instead.
*
* @param type
* @param path
* @return
*/
default PathPredicates matcherForSuitable(Class<? extends TargetObject> type,
List<String> path) {
PathMatcher result = new PathMatcher();
Set<TargetObjectSchema> visited = new HashSet<>();
List<TargetObjectSchema> schemas = getSuccessorSchemas(path);
for (; path != null; path = PathUtils.parent(path)) {
TargetObjectSchema schema = schemas.get(path.size());
Private.searchFor(schema, result, path, false, type, true, false, visited);
}
return result;
}
/**
* Like {@link #searchForSuitable(Class, List)}, but searches for the canonical container whose
* elements have the given type

View file

@ -327,28 +327,42 @@ public interface PathPredicates {
}
if (successorCouldMatch(path, true)) {
Set<String> nextNames = getNextNames(path);
if (!nextNames.isEmpty()) {
if (nextNames.equals(PathMatcher.WILD_SINGLETON)) {
for (Map.Entry<String, ?> ent : cur.getCachedAttributes().entrySet()) {
Object value = ent.getValue();
if (!(value instanceof TargetObject)) {
if (!(ent.getValue() instanceof TargetObject obj)) {
continue;
}
String name = ent.getKey();
if (!anyMatches(nextNames, name)) {
continue;
}
TargetObject obj = (TargetObject) value;
getCachedSuccessors(result, PathUtils.extend(path, name), obj);
}
}
else {
for (String name : nextNames) {
if (!(cur.getCachedAttribute(name) instanceof TargetObject obj)) {
continue;
}
getCachedSuccessors(result, PathUtils.extend(path, name), obj);
}
}
Set<String> nextIndices = getNextIndices(path);
if (!nextIndices.isEmpty()) {
if (nextIndices.equals(PathMatcher.WILD_SINGLETON)) {
for (Map.Entry<String, ? extends TargetObject> ent : cur.getCachedElements()
.entrySet()) {
TargetObject obj = ent.getValue();
if (obj == null) {
return;
}
String index = ent.getKey();
if (!anyMatches(nextIndices, index)) {
continue;
getCachedSuccessors(result, PathUtils.index(path, index), obj);
}
}
else {
Map<String, ? extends TargetObject> elements = cur.getCachedElements();
for (String index : nextIndices) {
TargetObject obj = elements.get(index);
if (obj == null) {
return;
}
getCachedSuccessors(result, PathUtils.index(path, index), obj);
}

View file

@ -70,7 +70,7 @@ public class TestDebuggerObjectModel extends EmptyDebuggerObjectModel {
}
protected TestTargetSession newTestTargetSession(String rootHint) {
return new TestTargetSession(this, rootHint, ROOT_SCHEMA);
return new TestTargetSession(this, rootHint, getRootSchema());
}
protected TestTargetEnvironment newTestTargetEnvironment(TestTargetSession session) {

View file

@ -0,0 +1,251 @@
/* ###
* 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.dbg.target;
import static org.junit.Assert.*;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.junit.Test;
import ghidra.async.AsyncTestUtils;
import ghidra.async.AsyncUtils;
import ghidra.dbg.error.DebuggerIllegalArgumentException;
import ghidra.dbg.model.*;
import ghidra.dbg.target.TargetMethod.*;
public class TargetMethodTest implements AsyncTestUtils {
@Test
public void testAnnotatedMethodVoid0Args() throws Throwable {
TestDebuggerModelBuilder mb = new TestDebuggerModelBuilder() {
@Override
protected TestDebuggerObjectModel newModel(String typeHint) {
return new TestDebuggerObjectModel(typeHint) {
@Override
protected TestTargetThread newTestTargetThread(
TestTargetThreadContainer container, int tid) {
return new TestTargetThread(container, tid) {
{
changeAttributes(List.of(),
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
testModel, this),
"Methods");
}
@TargetMethod.Export("MyMethod")
public CompletableFuture<Void> myMethod() {
return AsyncUtils.NIL;
}
};
}
};
}
};
mb.createTestModel();
mb.createTestProcessesAndThreads();
TargetMethod method = (TargetMethod) mb.testThread1.getCachedAttribute("MyMethod");
assertEquals(Void.class, method.getReturnType());
assertEquals(TargetParameterMap.of(), method.getParameters());
assertNull(waitOn(method.invoke(Map.of())));
try {
waitOn(method.invoke(Map.ofEntries(Map.entry("p1", "err"))));
fail("Didn't catch extraneous argument");
}
catch (DebuggerIllegalArgumentException e) {
// pass
}
}
@Test
public void testAnnotatedMethodVoid1ArgBool() throws Throwable {
TestDebuggerModelBuilder mb = new TestDebuggerModelBuilder() {
@Override
protected TestDebuggerObjectModel newModel(String typeHint) {
return new TestDebuggerObjectModel(typeHint) {
@Override
protected TestTargetThread newTestTargetThread(
TestTargetThreadContainer container, int tid) {
return new TestTargetThread(container, tid) {
{
changeAttributes(List.of(),
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
testModel, this),
"Methods");
}
@TargetMethod.Export("MyMethod")
public CompletableFuture<Void> myMethod(
@TargetMethod.Param(
display = "P1",
description = "A boolean param",
name = "p1") boolean b) {
return AsyncUtils.NIL;
}
};
}
};
}
};
mb.createTestModel();
mb.createTestProcessesAndThreads();
TargetMethod method = (TargetMethod) mb.testThread1.getCachedAttribute("MyMethod");
assertEquals(Void.class, method.getReturnType());
assertEquals(TargetParameterMap.ofEntries(
Map.entry("p1",
ParameterDescription.create(Boolean.class, "p1", true, null, "P1",
"A boolean param"))),
method.getParameters());
assertNull(waitOn(method.invoke(Map.ofEntries(Map.entry("p1", true)))));
try {
waitOn(method.invoke(Map.ofEntries(Map.entry("p1", "err"))));
fail("Didn't catch type mismatch");
}
catch (DebuggerIllegalArgumentException e) {
// pass
}
try {
waitOn(method.invoke(Map.ofEntries(
Map.entry("p1", true),
Map.entry("p2", "err"))));
fail("Didn't catch extraneous argument");
}
catch (DebuggerIllegalArgumentException e) {
// pass
}
try {
waitOn(method.invoke(Map.ofEntries()));
fail("Didn't catch missing argument");
}
catch (DebuggerIllegalArgumentException e) {
// pass
}
}
@Test
public void testAnnotatedMethodString1ArgInt() throws Throwable {
TestDebuggerModelBuilder mb = new TestDebuggerModelBuilder() {
@Override
protected TestDebuggerObjectModel newModel(String typeHint) {
return new TestDebuggerObjectModel(typeHint) {
@Override
protected TestTargetThread newTestTargetThread(
TestTargetThreadContainer container, int tid) {
return new TestTargetThread(container, tid) {
{
changeAttributes(List.of(),
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
testModel, this),
"Methods");
}
@TargetMethod.Export("MyMethod")
public CompletableFuture<String> myMethod(
@TargetMethod.Param(
display = "P1",
description = "An int param",
name = "p1") int i) {
return CompletableFuture.completedFuture(Integer.toString(i));
}
};
}
};
}
};
mb.createTestModel();
mb.createTestProcessesAndThreads();
TargetMethod method = (TargetMethod) mb.testThread1.getCachedAttribute("MyMethod");
assertEquals(String.class, method.getReturnType());
assertEquals(TargetParameterMap.ofEntries(
Map.entry("p1",
ParameterDescription.create(Integer.class, "p1", true, null, "P1",
"An int param"))),
method.getParameters());
assertEquals("3", waitOn(method.invoke(Map.ofEntries(Map.entry("p1", 3)))));
}
@Test
public void testAnnotatedMethodStringManyArgs() throws Throwable {
TestDebuggerModelBuilder mb = new TestDebuggerModelBuilder() {
@Override
protected TestDebuggerObjectModel newModel(String typeHint) {
return new TestDebuggerObjectModel(typeHint) {
@Override
protected TestTargetThread newTestTargetThread(
TestTargetThreadContainer container, int tid) {
return new TestTargetThread(container, tid) {
{
changeAttributes(List.of(),
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
testModel, this),
"Methods");
}
@TargetMethod.Export("MyMethod")
public CompletableFuture<String> myMethod(
@TargetMethod.Param(
display = "I",
description = "An int param",
name = "i") int i,
@TargetMethod.Param(
display = "B",
description = "A boolean param",
name = "b") boolean b,
@TargetMethod.Param(
display = "S",
description = "A string param",
name = "s") String s,
@TargetMethod.Param(
display = "L",
description = "A long param",
name = "l") long l) {
return CompletableFuture
.completedFuture(i + "," + b + "," + s + "," + l);
}
};
}
};
}
};
mb.createTestModel();
mb.createTestProcessesAndThreads();
TargetMethod method = (TargetMethod) mb.testThread1.getCachedAttribute("MyMethod");
assertEquals(String.class, method.getReturnType());
assertEquals(TargetParameterMap.ofEntries(
Map.entry("i",
ParameterDescription.create(Integer.class, "i", true, null, "I",
"An int param")),
Map.entry("b",
ParameterDescription.create(Boolean.class, "b", true, null, "B",
"A boolean param")),
Map.entry("s",
ParameterDescription.create(String.class, "s", true, null, "S",
"A string param")),
Map.entry("l",
ParameterDescription.create(Long.class, "l", true, null, "L",
"A long param"))),
method.getParameters());
assertEquals("3,true,Hello,7", waitOn(method.invoke(Map.ofEntries(
Map.entry("b", true), Map.entry("i", 3), Map.entry("s", "Hello"),
Map.entry("l", 7L)))));
}
}

View file

@ -16,17 +16,18 @@
package ghidra.dbg.target.schema;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.junit.Test;
import ghidra.async.AsyncUtils;
import ghidra.dbg.agent.*;
import ghidra.dbg.target.*;
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.dbg.target.schema.TargetObjectSchema.*;
public class AnnotatedTargetObjectSchemaTest {
@ -384,4 +385,31 @@ public class AnnotatedTargetObjectSchemaTest {
AnnotatedSchemaContext ctx = new AnnotatedSchemaContext();
ctx.getSchemaForClass(TestAnnotatedTargetRootWithListedAttrsBadType.class);
}
@TargetObjectSchemaInfo
static class TestAnnotatedTargetRootWithExportedTargetMethod extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootWithExportedTargetMethod(AbstractDebuggerObjectModel model,
String typeHint) {
super(model, typeHint);
}
@TargetMethod.Export("MyMethod")
public CompletableFuture<Void> myMethod() {
return AsyncUtils.NIL;
}
}
@Test
public void testAnnotatedRootSchemaWithExportedTargetMethod() {
AnnotatedSchemaContext ctx = new AnnotatedSchemaContext();
TargetObjectSchema schema =
ctx.getSchemaForClass(TestAnnotatedTargetRootWithExportedTargetMethod.class);
AttributeSchema methodSchema = schema.getAttributeSchema("MyMethod");
assertEquals(
new DefaultAttributeSchema("MyMethod", new SchemaName("Method"), true, true, true),
methodSchema);
assertTrue(
ctx.getSchema(new SchemaName("Method")).getInterfaces().contains(TargetMethod.class));
}
}

View file

@ -411,4 +411,12 @@
<attribute name="_order" schema="INT" hidden="yes" />
<attribute schema="VOID" />
</schema>
</context>
<schema name="Method" elementResync="NEVER" attributeResync="NEVER">
<interface name="Method" />
<element schema="VOID" />
<attribute name="_display" schema="STRING" fixed="yes" hidden="yes" />
<attribute name="_return_type" schema="TYPE" fixed="yes" hidden="yes" />
<attribute name="_parameters" schema="MAP_PARAMETERS" fixed="yes" hidden="yes" />
<attribute schema="VOID" />
</schema>
</context>