diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/DbgThread.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/DbgThread.java index b2d94e3dd9..9981b436d9 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/DbgThread.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/DbgThread.java @@ -120,6 +120,22 @@ public interface DbgThread */ CompletableFuture step(Map 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 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 traceToAddress(String address); + /** * Detach from the entire process * diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/AbstractDbgExecToAddressCommand.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/AbstractDbgExecToAddressCommand.java new file mode 100644 index 0000000000..093c777994 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/AbstractDbgExecToAddressCommand.java @@ -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 { + + 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); + } + } + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgStepToAddressCommand.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgStepToAddressCommand.java new file mode 100644 index 0000000000..8f0f93f43d --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgStepToAddressCommand.java @@ -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; + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgTraceToAddressCommand.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgTraceToAddressCommand.java new file mode 100644 index 0000000000..c96cfabb14 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgTraceToAddressCommand.java @@ -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; + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgThreadImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgThreadImpl.java index d56ba78395..abd96b2bfe 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgThreadImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgThreadImpl.java @@ -225,6 +225,20 @@ public class DbgThreadImpl implements DbgThread { }); } + @Override + public CompletableFuture stepToAddress(String address) { + return setActive().thenCompose(__ -> { + return manager.execute(new DbgStepToAddressCommand(manager, id, address)); + }); + } + + @Override + public CompletableFuture traceToAddress(String address) { + return setActive().thenCompose(__ -> { + return manager.execute(new DbgTraceToAddressCommand(manager, id, address)); + }); + } + @Override public CompletableFuture kill() { return setActive().thenCompose(__ -> { diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetSteppable.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetSteppable.java index e33758393a..dfa70993f9 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetSteppable.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetSteppable.java @@ -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; diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetThread.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetThread.java index 9cf4c824d0..8663a6f270 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetThread.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetThread.java @@ -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 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 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 setActive() { DbgManagerImpl manager = getManager(); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessImpl.java index d0845d2156..eca72ea5da 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessImpl.java @@ -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))); } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetThreadImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetThreadImpl.java index 4b1d1d949f..e38554375b 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetThreadImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetThreadImpl.java @@ -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))); } diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetObjectImpl.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetObjectImpl.java index 7d345ffe22..6aa72afdd4 100644 --- a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetObjectImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetObjectImpl.java @@ -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 + @@ -364,8 +365,18 @@ + + + + + + diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbThread.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbThread.java index 47f7817d5c..cab6a3ddae 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbThread.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbThread.java @@ -123,6 +123,30 @@ public interface GdbThread */ CompletableFuture step(StepCmd suffix); + /** + * Advance the thread to the given location + * + *

+ * This is equivalent to the CLI command {@code advance}. + * + *

+ * 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 advance(String loc); + + /** + * Advance the thread to the given address + * + * @param addr the address + * @see #advance(String) + */ + CompletableFuture advance(long addr); + /** * Detach from the entire process * diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/breakpoint/GdbBreakpointInsertions.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/breakpoint/GdbBreakpointInsertions.java index 775f6e56c0..551e83fd4a 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/breakpoint/GdbBreakpointInsertions.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/breakpoint/GdbBreakpointInsertions.java @@ -45,6 +45,7 @@ public interface GdbBreakpointInsertions { /** * Insert a breakpoint (usually a watchpoint) at the given address range * + *

* 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. diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbThreadImpl.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbThreadImpl.java index 4b09869bf1..c25acdff07 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbThreadImpl.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbThreadImpl.java @@ -286,6 +286,17 @@ public class GdbThreadImpl implements GdbThread { return execute(new GdbStepCommand(manager, id, suffix)); } + @Override + public CompletableFuture advance(String loc) { + // There's no exec-advance or similar in MI2.... + return console("advance " + loc, CompletesWithRunning.MUST); + } + + @Override + public CompletableFuture advance(long addr) { + return advance(String.format("*0x%x", addr)); + } + @Override public CompletableFuture kill() { return execute(new GdbKillCommand(manager, id)); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java index 55d52e3deb..fb748b1c0d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java @@ -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))); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java index 5d3eda319d..d61c59522c 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java @@ -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 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 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(); } - } diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/java/agent/lldb/manager/cmd/LldbRunToAddressCommand.java b/Ghidra/Debug/Debugger-agent-lldb/src/main/java/agent/lldb/manager/cmd/LldbRunToAddressCommand.java new file mode 100644 index 0000000000..0934ff5211 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/java/agent/lldb/manager/cmd/LldbRunToAddressCommand.java @@ -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 { + + 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()); + } + } +} diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/java/agent/lldb/manager/cmd/LldbStepCommand.java b/Ghidra/Debug/Debugger-agent-lldb/src/main/java/agent/lldb/manager/cmd/LldbStepCommand.java index 88326750a2..acbeefb21c 100644 --- a/Ghidra/Debug/Debugger-agent-lldb/src/main/java/agent/lldb/manager/cmd/LldbStepCommand.java +++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/java/agent/lldb/manager/cmd/LldbStepCommand.java @@ -86,11 +86,6 @@ public class LldbStepCommand extends AbstractLldbCommand { 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; diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/java/agent/lldb/model/impl/LldbModelTargetThreadImpl.java b/Ghidra/Debug/Debugger-agent-lldb/src/main/java/agent/lldb/model/impl/LldbModelTargetThreadImpl.java index d08d861858..6c3de4a608 100644 --- a/Ghidra/Debug/Debugger-agent-lldb/src/main/java/agent/lldb/model/impl/LldbModelTargetThreadImpl.java +++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/java/agent/lldb/model/impl/LldbModelTargetThreadImpl.java @@ -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 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 step(Map 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 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 diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpValueUtils.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpValueUtils.java index 8cc9935825..5a5001f759 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpValueUtils.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpValueUtils.java @@ -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: diff --git a/Ghidra/Debug/Debugger-gadp/src/main/proto/gadp.proto b/Ghidra/Debug/Debugger-gadp/src/main/proto/gadp.proto index ee6cff381b..920ec60e1d 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/proto/gadp.proto +++ b/Ghidra/Debug/Debugger-gadp/src/main/proto/gadp.proto @@ -90,7 +90,6 @@ message BreakKindsSet { enum StepKind { SK_INTO = 0; - SK_ADVANCE = 1; SK_FINISH = 2; SK_LINE = 3; SK_OVER = 4; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThread.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThread.java index df483e025a..02e8cfeffa 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThread.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThread.java @@ -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: diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java index 6af757ef6c..8e84fb196f 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java @@ -612,9 +612,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin public DebuggerControlPlugin(PluginTool tool) { super(tool); - tool.addContextListener(this); - createActions(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerMethodActionsPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerMethodActionsPlugin.java new file mode 100644 index 0000000000..1bbfc3ebf8 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerMethodActionsPlugin.java @@ -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 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 getPopupActions(Tool tool, ActionContext context) { + TargetObject curObj = getCurrentTargetObject(); + if (curObj == null) { + return List.of(); + } + List 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 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 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; + } +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerMethodActionsPluginTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerMethodActionsPluginTest.java new file mode 100644 index 0000000000..309ab67d29 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerMethodActionsPluginTest.java @@ -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 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 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 stepExt() { + commands.add("stepExt"); + return AsyncUtils.NIL; + } + + // Takes a second required non-default parameter + @TargetMethod.Export("AdvanceWithFlag") + public CompletableFuture 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 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 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); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMethod.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMethod.java index 79648f77b8..e144efd828 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMethod.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMethod.java @@ -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 { + boolean specified(); + + T value(); + } + + @interface BoolValue { + boolean specified() default true; + + boolean value(); + + record Val(BoolValue v) implements Value { + @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 { + @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 { + @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 { + @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 { + @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 { + @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 { + @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> { + @Override + public boolean specified() { + return v.specified(); + } + + @Override + public List value() { + return List.of(v.value()); + } + } + } + + // TODO: Address, Range, BreakKind[Set], etc? + + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @interface Param { + List>> 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 * - *

- * TODO: For convenience, these should be programmable via annotations. *

* 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 defaults = new ArrayList<>(); + for (Function> 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 getDefault(Class 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 ParameterDescription annotated(Class 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 type; public final String name; public final T defaultValue; @@ -199,13 +486,78 @@ public interface TargetMethod extends TargetObject { public static TargetParameterMap ofEntries( Entry>... entries) { Map> ordered = new LinkedHashMap<>(); - for (Entry> ent: entries) { + for (Entry> ent : entries) { ordered.put(ent.getKey(), ent.getValue()); } return new ImmutableTargetParameterMap(ordered); } } + class AnnotatedTargetMethod extends DefaultTargetObject + implements TargetMethod { + + public static Map collectExports(Lookup lookup, + AbstractDebuggerObjectModel model, TargetObject parent) { + Map result = new HashMap<>(); + Set> 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, 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 invoke(Map arguments) { + Map valid = TargetMethod.validateArguments(params, arguments, false); + List args = new ArrayList<>(params.size()); + for (ParameterDescription p : params.values()) { + args.add(p.get(valid)); + } + try { + return (CompletableFuture) handle.invokeWithArguments(args); + } + catch (Throwable e) { + return CompletableFuture.failedFuture(e); + } + } + } + /** * Construct a map of parameter descriptions from a stream * diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetSteppable.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetSteppable.java index 79dda01364..beb64e8430 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetSteppable.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetSteppable.java @@ -66,31 +66,12 @@ public interface TargetSteppable extends TargetObject { } enum TargetStepKind { - /** - * Step strictly forward - * - *

- * To avoid runaway execution, stepping should cease if execution returns from the current - * frame. - * - *

- * 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. * *

* In more detail: step until the object has executed the return instruction that returns * from the current frame. - * - *

- * 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, /** diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/AnnotatedSchemaContext.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/AnnotatedSchemaContext.java index f6691f1e27..dcb8ac7870 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/AnnotatedSchemaContext.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/AnnotatedSchemaContext.java @@ -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 declCls, Class 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 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 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 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; } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultSchemaContext.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultSchemaContext.java index f4d02aa401..fbc0bfb61e 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultSchemaContext.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultSchemaContext.java @@ -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); diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultTargetObjectSchema.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultTargetObjectSchema.java index 0f47b86785..bd714d1835 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultTargetObjectSchema.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultTargetObjectSchema.java @@ -24,7 +24,7 @@ public class DefaultTargetObjectSchema implements TargetObjectSchema, Comparable { private static final String INDENT = " "; - protected static class DefaultAttributeSchema + public static class DefaultAttributeSchema implements AttributeSchema, Comparable { private final String name; private final SchemaName schema; diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java index 7f862332b7..62df310f2f 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java @@ -36,7 +36,7 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema { *

* 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 { *

* 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> 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); } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/SchemaBuilder.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/SchemaBuilder.java index 0c73051e86..b7bc02d4a0 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/SchemaBuilder.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/SchemaBuilder.java @@ -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; diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java index 2155534397..eaa81f4cd4 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java @@ -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 prefix, boolean parentIsCanonical, Class type, - boolean requireCanonical, Set visited) { + boolean requireAggregate, boolean requireCanonical, + Set 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 ent : sch.getElementSchemas().entrySet()) { List 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 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 ent : sch.getAttributeSchemas().entrySet()) { List 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 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 + * + *

+ * 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 not 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 type, + List path) { + PathMatcher result = new PathMatcher(); + Set visited = new HashSet<>(); + List 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 diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java index 1a968a8c8f..46f14a18e1 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java @@ -327,28 +327,42 @@ public interface PathPredicates { } if (successorCouldMatch(path, true)) { Set nextNames = getNextNames(path); - if (!nextNames.isEmpty()) { + if (nextNames.equals(PathMatcher.WILD_SINGLETON)) { for (Map.Entry 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 nextIndices = getNextIndices(path); - if (!nextIndices.isEmpty()) { + if (nextIndices.equals(PathMatcher.WILD_SINGLETON)) { for (Map.Entry 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 elements = cur.getCachedElements(); + for (String index : nextIndices) { + TargetObject obj = elements.get(index); + if (obj == null) { + return; } getCachedSuccessors(result, PathUtils.index(path, index), obj); } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java index 8114db751a..d16a415f23 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java @@ -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) { diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/TargetMethodTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/TargetMethodTest.java new file mode 100644 index 0000000000..cac211a928 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/TargetMethodTest.java @@ -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 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 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 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 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))))); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/AnnotatedTargetObjectSchemaTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/AnnotatedTargetObjectSchemaTest.java index 6d14f6fd7d..a79c7cd687 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/AnnotatedTargetObjectSchemaTest.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/AnnotatedTargetObjectSchemaTest.java @@ -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 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)); + } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml b/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml index cd3f019e05..8a3eebeb83 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml +++ b/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml @@ -411,4 +411,12 @@