GP-5972: post-review

GP-5792: minimize calls while running
GP-5972: better tx refs
GP-5972: fixes for typing errors
GP-5972: temp mods
GP-5972: vscode mods
GP-5972: post-review
GP-5972: post-review
GP-5972: updates docs
GP-5972: support for 32-bit
GP-5972: mods post-PR
GP-5972: better error handling
GP-5972: ss-only
GP-5972: help
GP-5972: help
GP-5972: mostly functional tests
GP-5972: lame hook tests
GP-5972: methods pass
GP-5972: first pass - works a little
GP5972: some functionalityGP-5972: mem+GP-5921: basicsGP-5921: pc, spGP-5972: regsGP-5972: regs/mem/modsGP-5972: ghidraxdbgGP-5972: _base -> clientGP-5972: mods/memGP-5972: availableGP-5972: bptsGP-5972: -pybagGP-5972: initial stateGP-5972: misc fixesGP-5972: bptsGP-5972: various methods/slightly better stateGP-5972: del bptsGP-5972: more bptsGP-5972: better attachGP-5972: better launchGP-5972: tests round 0GP-5972: cmd tests - setsGP-5972: cmd tests passGP-5972: methodsGP-5972: methods exc 3
GP-5972: x64dbg init
This commit is contained in:
d-millar
2025-11-12 12:18:40 -05:00
parent 38043db812
commit e28c2ead26
26 changed files with 6854 additions and 0 deletions

View File

@@ -0,0 +1,512 @@
/* ###
* 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.x64dbg.rmi;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeTrue;
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Function;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.junit.Before;
import org.junit.BeforeClass;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin;
import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
import ghidra.app.services.TraceRmiService;
import ghidra.debug.api.tracermi.*;
import ghidra.framework.*;
import ghidra.framework.main.ApplicationLevelOnlyPlugin;
import ghidra.framework.model.DomainFile;
import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.PluginsConfiguration;
import ghidra.framework.plugintool.util.*;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRangeImpl;
import ghidra.pty.testutil.DummyProc;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.breakpoint.TraceBreakpointKind;
import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
import ghidra.trace.model.target.TraceObject;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.util.*;
public abstract class AbstractX64dbgTraceRmiTest extends AbstractGhidraHeadedDebuggerTest {
/**
* Some features have to be disabled to avoid permissions issues in the test container. Namely,
* don't try to disable ASLR.
*/
public static final String PREAMBLE = """
from ghidraxdbg.commands import *
from ghidratrace.client import Schedule
from x64dbg_automate.models import *
""";
// Connecting should be the first thing the script does, so use a tight timeout.
protected static final int CONNECT_TIMEOUT_MS = 3000;
protected static final int TIMEOUT_SECONDS = 300;
protected static final int QUIT_TIMEOUT_MS = 1000;
/** Some snapshot likely to exceed the latest */
protected static final long SNAP = 100;
protected static boolean didSetupPython = false;
public static final String NOTEPAD = "C:\\\\Windows\\\\notepad.exe";
public static final String INSTRUMENT_STATE = """
import sys
from ghidraxdbg import commands
from x64dbg_automate.events import *
print("Instrumenting")
def on_state_changed(*args):
print("State changed")
sys.stdout.flush()
proc = util.selected_process()
trace = commands.STATE.trace
with commands.STATE.client.batch():
with trace.open_tx("State changed proc {}".format(proc)):
commands.put_state(proc)
return
def install_hooks():
print("Installing")
util.dbg.client.watch_debug_event(EventType.EVENT_DEBUG, lambda x: on_state_changed(x))
install_hooks()
""";
protected TraceRmiService traceRmi;
private Path pythonPath;
private Path outFile;
private Path errFile;
@BeforeClass
public static void setupPython() throws Throwable {
if (didSetupPython) {
// Only do this once when running the full suite.
return;
}
if (SystemUtilities.isInTestingBatchMode()) {
// Don't run gradle in gradle. It already did this task.
return;
}
String gradle = DummyProc.which("gradle.bat");
new ProcessBuilder(gradle, "assemblePyPackage")
.directory(TestApplicationUtils.getInstallationDirectory())
.inheritIO()
.start()
.waitFor();
didSetupPython = true;
}
protected void setPythonPath(ProcessBuilder pb) throws IOException {
String sep =
OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS ? ";" : ":";
String rmiPyPkg = Application.getModuleSubDirectory("Debugger-rmi-trace",
"build/pypkg/src").getAbsolutePath();
String gdbPyPkg = Application.getModuleSubDirectory("Debugger-agent-x64dbg",
"build/pypkg/src").getAbsolutePath();
String add = rmiPyPkg + sep + gdbPyPkg;
pb.environment().compute("PYTHONPATH", (k, v) -> v == null ? add : (v + sep + add));
}
protected void setX64dbgPath(ProcessBuilder pb) throws IOException {
pb.environment().put("OPT_X64DBG_EXE", "C:\\Software\\snapshot_2025-08-19_19-40\\release\\x64\\x64dbg.exe");
}
@BeforeClass
public static void assertOS() {
assumeTrue("Not on Windows",
OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS);
}
@Before
public void setupTraceRmi() throws Throwable {
traceRmi = addPlugin(tool, TraceRmiPlugin.class);
try {
pythonPath = Paths.get(DummyProc.which("python3"));
}
catch (RuntimeException e) {
pythonPath = Paths.get(DummyProc.which("python"));
}
pythonPath = new File("/C:/Python313/python.exe").toPath();
assertTrue(pythonPath.toFile().exists());
outFile = Files.createTempFile("pydbgout", null);
errFile = Files.createTempFile("pydbgerr", null);
}
protected void addAllDebuggerPlugins() throws PluginException {
PluginsConfiguration plugConf = new PluginsConfiguration() {
@Override
protected boolean accepts(Class<? extends Plugin> pluginClass) {
return !ApplicationLevelOnlyPlugin.class.isAssignableFrom(pluginClass);
}
};
for (PluginDescription pd : plugConf
.getPluginDescriptions(PluginPackage.getPluginPackage("Debugger"))) {
addPlugin(tool, pd.getPluginClass());
}
}
protected static String addrToStringForPython(InetAddress address) {
if (address.isAnyLocalAddress()) {
return "127.0.0.1"; // Can't connect to 0.0.0.0 as such. Choose localhost.
}
return address.getHostAddress();
}
protected static String sockToStringForPython(SocketAddress address) {
if (address instanceof InetSocketAddress tcp) {
return addrToStringForPython(tcp.getAddress()) + ":" + tcp.getPort();
}
throw new AssertionError("Unhandled address type " + address);
}
protected record PythonResult(boolean timedOut, int exitCode, String stdout, String stderr) {
protected String handle() {
if (stderr.contains("Error") || (0 != exitCode && 1 != exitCode && 143 != exitCode)) {
throw new PythonError(exitCode, stdout, stderr);
}
System.out.println("--stdout--");
System.out.println(stdout);
System.out.println("--stderr--");
System.out.println(stderr);
return stdout;
}
}
protected record ExecInPython(Process python, CompletableFuture<PythonResult> future) {}
protected void pump(InputStream streamIn, OutputStream streamOut) {
Thread t = new Thread(() -> {
try (PrintStream printOut = new PrintStream(streamOut);
BufferedReader reader = new BufferedReader(new InputStreamReader(streamIn))) {
String line;
while ((line = reader.readLine()) != null) {
printOut.println(line);
printOut.flush();
}
}
catch (IOException e) {
Msg.info(this, "Terminating stdin pump, because " + e);
}
});
t.setDaemon(true);
t.start();
}
protected void pumpTee(InputStream streamIn, File fileOut, PrintStream streamOut) {
Thread t = new Thread(() -> {
try (PrintStream fileStream = new PrintStream(fileOut);
BufferedReader reader = new BufferedReader(new InputStreamReader(streamIn))) {
String line;
while ((line = reader.readLine()) != null) {
streamOut.println(line);
streamOut.flush();
fileStream.println(line);
fileStream.flush();
}
}
catch (IOException e) {
Msg.info(this, "Terminating tee: " + fileOut + ", because " + e);
}
});
t.setDaemon(true);
t.start();
}
@SuppressWarnings("resource") // Do not close stdin
protected ExecInPython execInPython(String script) throws IOException {
ProcessBuilder pb = new ProcessBuilder(pythonPath.toString(), "-i");
setPythonPath(pb);
setX64dbgPath(pb);
// If commands come from file, Python will quit after EOF.
Msg.info(this, "outFile: " + outFile);
Msg.info(this, "errFile: " + errFile);
//pb.inheritIO();
pb.redirectInput(ProcessBuilder.Redirect.PIPE);
if (SystemUtilities.isInTestingBatchMode()) {
pb.redirectOutput(outFile.toFile());
pb.redirectError(errFile.toFile());
}
else {
pb.redirectOutput(ProcessBuilder.Redirect.PIPE);
pb.redirectError(ProcessBuilder.Redirect.PIPE);
}
Process pyproc = pb.start();
if (!SystemUtilities.isInTestingBatchMode()) {
pumpTee(pyproc.getInputStream(), outFile.toFile(), System.out);
pumpTee(pyproc.getErrorStream(), errFile.toFile(), System.err);
}
OutputStream stdin = pyproc.getOutputStream();
stdin.write(script.getBytes());
stdin.flush();
if (!SystemUtilities.isInTestingBatchMode()) {
pump(System.in, stdin);
}
return new ExecInPython(pyproc, CompletableFuture.supplyAsync(() -> {
try {
if (!pyproc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
Msg.error(this, "Timed out waiting for Python");
pyproc.destroyForcibly();
pyproc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS);
return new PythonResult(true, -1, Files.readString(outFile),
Files.readString(errFile));
}
Msg.info(this, "Python exited with code " + pyproc.exitValue());
return new PythonResult(false, pyproc.exitValue(), Files.readString(outFile),
Files.readString(errFile));
}
catch (Exception e) {
return ExceptionUtils.rethrow(e);
}
finally {
pyproc.destroyForcibly();
}
}));
}
public static class PythonError extends RuntimeException {
public final int exitCode;
public final String stdout;
public final String stderr;
public PythonError(int exitCode, String stdout, String stderr) {
super("""
exitCode=%d:
----stdout----
%s
----stderr----
%s
""".formatted(exitCode, stdout, stderr));
this.exitCode = exitCode;
this.stdout = stdout;
this.stderr = stderr;
}
}
protected String runThrowError(String script) throws Exception {
CompletableFuture<PythonResult> result = execInPython(script).future;
return result.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle();
}
protected record PythonAndConnection(ExecInPython exec, TraceRmiConnection connection)
implements AutoCloseable {
protected RemoteMethod getMethod(String name) {
return Objects.requireNonNull(connection.getMethods().get(name));
}
public void execute(String cmd) {
RemoteMethod execute = getMethod("execute");
try {
execute.invoke(Map.of("cmd", cmd));
} catch (Exception e) {
Msg.warn(this, e.getMessage());
}
}
public RemoteAsyncResult executeAsync(String cmd) {
RemoteMethod execute = getMethod("execute");
return execute.invokeAsync(Map.of("cmd", cmd));
}
public String executeCapture(String cmd) {
RemoteMethod execute = getMethod("execute");
return (String) execute.invoke(Map.of("cmd", cmd, "to_string", true));
}
@Override
public void close() throws Exception {
Msg.info(this, "Cleaning up python");
exec.python().destroy();
try {
PythonResult r = exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
r.handle();
waitForPass(this, () -> assertTrue(connection.isClosed()),
TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
finally {
exec.python.destroyForcibly();
}
}
}
protected PythonAndConnection startAndConnectPython(Function<String, String> scriptSupplier)
throws Exception {
TraceRmiAcceptor acceptor = traceRmi.acceptOne(null);
ExecInPython exec =
execInPython(scriptSupplier.apply(sockToStringForPython(acceptor.getAddress())));
acceptor.setTimeout(CONNECT_TIMEOUT_MS);
try {
TraceRmiConnection connection = acceptor.accept();
return new PythonAndConnection(exec, connection);
}
catch (SocketTimeoutException e) {
exec.python.destroyForcibly();
exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle();
throw e;
}
}
protected PythonAndConnection startAndConnectPython() throws Exception {
return startAndConnectPython(addr -> """
%s
ghidra_trace_connect('%s')
""".formatted(PREAMBLE, addr));
}
@SuppressWarnings("resource")
protected String runThrowError(Function<String, String> scriptSupplier)
throws Exception {
PythonAndConnection conn = startAndConnectPython(scriptSupplier);
PythonResult r = conn.exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
String stdout = r.handle();
waitForPass(this, () -> assertTrue(conn.connection.isClosed()),
TIMEOUT_SECONDS, TimeUnit.SECONDS);
return stdout;
}
protected void waitStopped(String message) {
TraceObject proc =
Objects.requireNonNull(tb.objAny("Sessions[].Processes[]", Lifespan.at(0)));
waitForPass(() -> assertEquals(message, "STOPPED", tb.objValue(proc, 0, "_state")));
waitTxDone();
}
protected void waitRunning(String message) {
TraceObject proc =
Objects.requireNonNull(tb.objAny("Sessions[].Processes[]", Lifespan.at(0)));
waitForPass(() -> assertEquals(message, "RUNNING", tb.objValue(proc, 0, "_state")));
waitTxDone();
}
protected String extractOutSection(String out, String head) {
String[] split = out.split("\n");
String xout = "";
for (String s : split) {
if (!s.startsWith("(python)") && !s.equals("")) {
xout += s + "\n";
}
}
return xout.split(head)[1].split("---")[0].replace("(python)", "").trim();
}
record MemDump(long address, byte[] data) {}
protected MemDump parseHexDump(String dump) throws IOException {
// First, get the address. Assume contiguous, so only need top line.
List<String> lines = List.of(dump.split("\n"));
List<String> toksLine0 = List.of(lines.get(0).split("\\s+"));
String addrstr = toksLine0.get(0);
if (addrstr.contains(":")) {
addrstr = addrstr.substring(0, addrstr.indexOf(":"));
}
long address = Long.parseLong(addrstr, 16);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
for (String l : lines) {
List<String> parts = List.of(l.split(":"));
assertEquals(2, parts.size());
String hex = parts.get(1).substring(0, 48);
byte[] lineData = NumericUtilities.convertStringToBytes(hex);
assertNotNull("Converted to null: " + hex, parts.get(1));
buf.write(lineData);
}
return new MemDump(address, buf.toByteArray());
}
record RegDump() {}
protected RegDump parseRegDump(String dump) {
return new RegDump();
}
protected ManagedDomainObject openDomainObject(String path) throws Exception {
DomainFile df = env.getProject().getProjectData().getFile(path);
assertNotNull(df);
return new ManagedDomainObject(df, false, false, monitor);
}
protected ManagedDomainObject waitDomainObject(String path) throws Exception {
DomainFile df;
long start = System.currentTimeMillis();
while (true) {
df = env.getProject().getProjectData().getFile(path);
if (df != null) {
return new ManagedDomainObject(df, false, false, monitor);
}
Thread.sleep(1000);
if (System.currentTimeMillis() - start > 30000) {
throw new TimeoutException("30 seconds expired waiting for domain file");
}
}
}
protected void assertBreakLoc(TraceObjectValue locVal, Address addr, int len, String type) throws Exception {
TraceObject loc = locVal.getChild();
TraceObject spec = loc;
assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue());
assertEquals(type, spec.getValue(0, "Type").getValue());
}
protected void assertWatchLoc(TraceObjectValue locVal, Address addr, int len, String type) throws Exception {
TraceObject loc = locVal.getChild();
assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue());
assertEquals(type, loc.getValue(0, "TypeEx").getValue());
}
protected void waitTxDone() {
waitFor(() -> tb.trace.getCurrentTransactionInfo() == null);
}
public static void waitForPass(Runnable runnable, long timeoutMs, long retryDelayMs) {
long start = System.currentTimeMillis();
AssertionError lastError = null;
while (System.currentTimeMillis() - start < timeoutMs) {
try {
runnable.run();
return;
}
catch (AssertionError e) {
lastError = e;
}
try {
Thread.sleep(retryDelayMs);
}
catch (InterruptedException e) {
// Retry sooner, I guess.
}
}
if (lastError == null) {
throw new AssertionError("Timed out before first try?");
}
throw lastError;
}
}

View File

@@ -0,0 +1,399 @@
/* ###
* 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.x64dbg.rmi;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.util.*;
import org.junit.Test;
import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
import ghidra.debug.api.tracermi.RemoteMethod;
import ghidra.program.model.address.AddressSpace;
import ghidra.trace.database.ToyDBTraceBuilder;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.memory.TraceMemorySpace;
import ghidra.trace.model.target.TraceObject;
import ghidra.trace.model.target.path.*;
import ghidra.trace.model.time.TraceSnapshot;
public class X64dbgHooksTest extends AbstractX64dbgTraceRmiTest {
private static final long RUN_TIMEOUT_MS = 5000;
private static final long RETRY_MS = 500;
record PythonAndTrace(PythonAndConnection conn, ManagedDomainObject mdo)
implements AutoCloseable {
public void execute(String cmd) {
conn.execute(cmd);
}
public String executeCapture(String cmd) {
return conn.executeCapture(cmd);
}
@Override
public void close() throws Exception {
try {
conn.execute("util.terminate_session()");
conn.close();
} catch (Exception e) {
//IGNORE
}
try {
mdo.close();
} catch (Exception e) {
//IGNORE
}
}
}
@SuppressWarnings("resource")
protected PythonAndTrace startAndSyncPython(String exec) throws Exception {
PythonAndConnection conn = startAndConnectPython();
try {
ManagedDomainObject mdo;
conn.execute("from ghidraxdbg.commands import *");
conn.execute(
"util.set_convenience_variable('ghidra-language', 'x86:LE:64:default')");
if (exec != null) {
start(conn, exec);
mdo = waitDomainObject("/New Traces/x64dbg/" + exec.substring(exec.lastIndexOf("\\")+1));
}
else {
conn.execute("ghidra_trace_start()");
mdo = waitDomainObject("/New Traces/x64dbg/noname");
}
clearBreakpoints(conn);
tb = new ToyDBTraceBuilder((Trace) mdo.get());
return new PythonAndTrace(conn, mdo);
}
catch (Exception e) {
clearBreakpoints(conn);
conn.execute("util.terminate_session()");
conn.close();
throw e;
}
}
protected long lastSnap(PythonAndTrace conn) {
return conn.conn.connection().getLastSnapshot(tb.trace);
}
static final int INIT_NOTEPAD_THREAD_COUNT = 4; // This could be fragile
//@Test - doesn't generate more than the initial 4
public void testOnNewThread() throws Exception {
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
conn.execute("from ghidraxdbg.commands import *");
txPut(conn, "processes");
waitForPass(() -> {
TraceObject proc = tb.objAny0("Sessions[].Processes[]");
assertNotNull(proc);
assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
txPut(conn, "threads");
waitForPass(() -> assertEquals(INIT_NOTEPAD_THREAD_COUNT,
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Threads[]").size()),
RUN_TIMEOUT_MS, RETRY_MS);
// Via method, go is asynchronous
RemoteMethod go = conn.conn.getMethod("go");
TraceObject proc = tb.objAny0("Sessions[].Processes[]");
go.invoke(Map.of("process", proc)); // Initial breakpoint
go.invoke(Map.of("process", proc));
waitForPass(() -> assertThat(
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Threads[]").size(),
greaterThan(INIT_NOTEPAD_THREAD_COUNT)),
RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnNewModule() throws Exception {
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
conn.execute("from ghidraxdbg.commands import *");
txPut(conn, "processes");
TraceObject proc = tb.objAny0("Sessions[].Processes[]");
waitForPass(() -> {
assertNotNull(proc);
assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
txPut(conn, "modules");
waitForPass(() -> assertThat(
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Modules[]").size(),
greaterThan(0)),
RUN_TIMEOUT_MS, RETRY_MS);
int size = tb.objValues(lastSnap(conn), "Sessions[].Processes[].Modules[]").size();
// Via method, go is asynchronous
RemoteMethod go = conn.conn.getMethod("go");
go.invoke(Map.of("process", proc)); // Initial breakpoint
go.invoke(Map.of("process", proc));
waitForPass(() -> assertThat(
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Modules[]").size(),
greaterThan(size)),
RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnThreadSelected() throws Exception {
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
txPut(conn, "processes");
conn.execute("util.dbg.client.stepi()"); // no initial event
waitForPass(() -> {
TraceObject proc = tb.objAny0("Sessions[0].Processes[]");
assertNotNull(proc);
assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
txPut(conn, "threads");
waitForPass(() -> {
List<Object> values = tb.objValues(lastSnap(conn), "Sessions[0].Processes[].Threads[]");
assertEquals(INIT_NOTEPAD_THREAD_COUNT, values.size());
}, RUN_TIMEOUT_MS, RETRY_MS);
// Now the real test
List<Object> values = tb.objValues(lastSnap(conn), "Sessions[0].Processes[].Threads[]");
TraceObject thread = (TraceObject) values.get(0);
Object tid0 = tb.objValue(thread, lastSnap(conn), "TID");
conn.execute("util.select_thread("+tid0.toString()+")");
waitForPass(() -> {
String tnum = conn.executeCapture("print(util.selected_thread())").strip();
assertEquals(tid0.toString(), tnum);
}, RUN_TIMEOUT_MS, RETRY_MS);
thread = (TraceObject) values.get(1);
Object tid1 = tb.objValue(thread, lastSnap(conn), "TID");
conn.execute("util.select_thread("+tid1.toString()+")");
waitForPass(() -> {
String tnum = conn.executeCapture("print(util.selected_thread())").strip();
assertEquals(tid1.toString(), tnum);
}, RUN_TIMEOUT_MS, RETRY_MS);
thread = (TraceObject) values.get(2);
Object tid2 = tb.objValue(thread, lastSnap(conn), "TID");
conn.execute("util.select_thread("+tid2.toString()+")");
waitForPass(() -> {
String tnum = conn.executeCapture("print(util.selected_thread())").strip();
assertEquals(tid2.toString(), tnum);
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
protected String getIndex(TraceObject object, String pattern, int n) {
if (object == null) {
return null;
}
PathPattern pat = PathFilter.parse(pattern).getSingletonPattern();
KeyPath path = object.getCanonicalPath();
if (path.size() < pat.asPath().size()) {
return null;
}
List<String> matched = pat.matchKeys(path, false);
if (matched == null) {
return null;
}
if (matched.size() <= n) {
return null;
}
return matched.get(n);
}
protected String threadIndex(TraceObject object) {
return getIndex(object, "Sessions[].Processes[].Threads[]", 2);
}
protected String frameIndex(TraceObject object) {
return getIndex(object, "Sessions[].Processes[].Threads[].Stack.Frames[]", 3);
}
@Test
public void testOnRegisterChanged() throws Exception {
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
conn.execute("ghidra_trace_txstart('Tx')");
conn.execute("ghidra_trace_putreg()");
conn.execute("ghidra_trace_txcommit()");
conn.execute("util.dbg.cmd('rax=0x1234')");
conn.execute("util.dbg.client.stepi()"); // no real event for register changes
String path = "Sessions[].Processes[].Threads[].Registers";
TraceObject registers = Objects.requireNonNull(tb.objAny(path, Lifespan.at(0)));
AddressSpace space = tb.trace.getBaseAddressFactory()
.getAddressSpace(registers.getCanonicalPath().toString());
TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(space, false);
waitForPass(() -> assertEquals("1234",
regs.getValue(lastSnap(conn), tb.reg("RAX")).getUnsignedValue().toString(16)));
}
}
@Test
public void testOnCont() throws Exception {
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
txPut(conn, "processes");
conn.execute("util.dbg.client.go()");
conn.execute("util.dbg.client.go()");
TraceObject proc = waitForValue(() -> tb.objAny0("Sessions[].Processes[]"));
waitForPass(() -> {
assertEquals("RUNNING", tb.objValue(proc, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnStop() throws Exception {
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
txPut(conn, "processes");
TraceObject proc = waitForValue(() -> tb.objAny0("Sessions[].Processes[]"));
waitForPass(() -> {
conn.execute("util.terminate_session()");
assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
//@Test - TODO: currently missing relevant events
public void testOnExited() throws Exception {
try (PythonAndTrace conn = startAndSyncPython("netstat.exe")) {
txPut(conn, "processes");
// Do the synchronous wait here, since netstat should terminate
conn.execute("util.dbg.client.go()");
waitForPass(() -> {
TraceSnapshot snapshot =
tb.trace.getTimeManager().getSnapshot(lastSnap(conn), false);
assertNotNull(snapshot);
assertEquals("Exited with code 0", snapshot.getDescription());
TraceObject proc = tb.objAny0("Sessions[].Processes[]");
assertNotNull(proc);
Object val = tb.objValue(proc, lastSnap(conn), "_exit_code");
assertThat(val, instanceOf(Number.class));
assertEquals(0, ((Number) val).longValue());
conn.execute("util.terminate_session()");
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnBreakpointCreated() throws Exception {
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
txPut(conn, "breakpoints");
assertEquals(0,
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]").size());
conn.execute("pc = util.get_pc()");
conn.execute("util.dbg.client.set_breakpoint(address_or_symbol=pc)");
conn.execute("util.dbg.client.stepi()"); // no real event for bpt changes
waitForPass(() -> {
List<Object> brks =
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]");
assertEquals(1, brks.size());
});
}
}
//@Test - works but has timing issues
public void testOnBreakpointModified() throws Exception {
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
txPut(conn, "breakpoints");
assertEquals(0,
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]").size());
conn.execute("pc = util.get_pc()");
conn.execute("util.dbg.client.set_breakpoint(address_or_symbol=pc)");
conn.execute("util.dbg.client.stepi()"); // no real event for bpt changes
TraceObject brk = waitForPass(() -> {
List<Object> brks =
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]");
assertEquals(1, brks.size());
return (TraceObject) brks.get(0);
});
assertEquals(true, tb.objValue(brk, lastSnap(conn), "Enabled"));
conn.execute("util.dbg.client.toggle_breakpoint(address_name_symbol_or_none=pc, on=False)");
conn.execute("util.dbg.client.stepi()");
conn.execute("util.dbg.client.wait_until_stopped()");
conn.execute("util.dbg.client.stepi()");
assertEquals(false, tb.objValue(brk, lastSnap(conn), "Enabled"));
}
}
@Test
public void testOnBreakpointDeleted() throws Exception {
try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
txPut(conn, "breakpoints");
assertEquals(0,
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]").size());
conn.execute("pc = util.get_pc()");
conn.execute("util.dbg.client.set_breakpoint(address_or_symbol=pc)");
conn.execute("util.dbg.client.stepi()"); // no real event for bpt changes
TraceObject brk = waitForPass(() -> {
List<Object> brks =
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]");
assertEquals(1, brks.size());
return (TraceObject) brks.get(0);
});
conn.execute("util.dbg.client.clear_breakpoint(address_name_symbol_or_none=pc)");
conn.execute("util.dbg.client.stepi()");
waitForPass(() -> assertEquals(0,
tb.objValues(lastSnap(conn), "Sessions[].Processes[].Debug.Software Breakpoints[]").size()));
}
}
private void start(PythonAndConnection conn, String obj) {
conn.execute("from ghidraxdbg.commands import *");
if (obj != null)
conn.execute("ghidra_trace_create('" + obj + "', wait=True)");
else
conn.execute("ghidra_trace_create()");
conn.execute("ghidra_trace_sync_enable()");
}
private void txPut(PythonAndTrace conn, String obj) {
conn.execute("ghidra_trace_txstart('Tx" + obj + "')");
conn.execute("ghidra_trace_put_" + obj + "()");
conn.execute("ghidra_trace_txcommit()");
}
private void clearBreakpoints(PythonAndConnection conn) {
conn.execute("util.dbg.client.clear_breakpoint(None)");
conn.execute("util.dbg.client.clear_hardware_breakpoint(None)");
conn.execute("util.dbg.client.clear_memory_breakpoint(None)");
}
}