mirror of
https://github.com/NationalSecurityAgency/ghidra
synced 2024-10-03 00:44:52 +00: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
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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(__ -> {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -90,7 +90,6 @@ message BreakKindsSet {
|
|||
|
||||
enum StepKind {
|
||||
SK_INTO = 0;
|
||||
SK_ADVANCE = 1;
|
||||
SK_FINISH = 2;
|
||||
SK_LINE = 3;
|
||||
SK_OVER = 4;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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,
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)))));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue