GP-3857: Port most Debugger components to TraceRmi.

This commit is contained in:
Dan 2023-11-02 10:43:31 -04:00
parent 7e4d2bcfaa
commit fd4380c07a
222 changed files with 7241 additions and 3752 deletions

View file

@ -1,71 +0,0 @@
/* ###
* 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 ghidraclass.debugger.screenshot;
import java.io.File;
import org.junit.*;
import db.Transaction;
import ghidra.app.util.importer.AutoImporter;
import ghidra.app.util.importer.MessageLog;
import ghidra.app.util.opinion.LoadResults;
import ghidra.base.project.GhidraProject;
import ghidra.framework.Application;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;
import ghidra.util.task.ConsoleTaskMonitor;
import ghidra.util.task.TaskMonitor;
public class TutorialDebuggerMaintenance extends AbstractGhidraHeadedIntegrationTest {
public static final TaskMonitor CONSOLE = new ConsoleTaskMonitor();
public PluginTool tool;
public TestEnv env;
public Program program;
@Before
public void setUp() throws Throwable {
env = new TestEnv();
tool = env.launchDefaultTool();
}
@After
public void tearDown() throws Throwable {
if (program != null) {
program.release(this);
program = null;
}
}
@Test
public void testRecreateTermminesGzf() throws Throwable {
File termmines = Application.getModuleDataFile("TestResources", "termmines").getFile(false);
LoadResults<Program> results = AutoImporter.importByUsingBestGuess(termmines,
env.getProject(), "/", this, new MessageLog(), CONSOLE);
program = results.getPrimaryDomainObject();
try (Transaction tx = program.openTransaction("Analyze")) {
program.setExecutablePath("/tmp/termmines");
GhidraProject.analyze(program);
}
File dest = new File(termmines.getParentFile(), "termmines.gzf");
dest.delete();
program.saveToPackedFile(dest, CONSOLE);
}
}

View file

@ -1,837 +0,0 @@
/* ###
* 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 ghidraclass.debugger.screenshot;
import static org.junit.Assert.assertTrue;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.io.*;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Test;
import db.Transaction;
import docking.action.DockingActionIf;
import docking.widgets.fieldpanel.FieldPanel;
import generic.Unique;
import generic.jar.ResourceFile;
import ghidra.app.cmd.disassemble.DisassembleCommand;
import ghidra.app.context.ProgramLocationActionContext;
import ghidra.app.decompiler.component.DecompilerPanel;
import ghidra.app.nav.Navigatable;
import ghidra.app.plugin.core.analysis.AutoAnalysisPlugin;
import ghidra.app.plugin.core.codebrowser.CodeViewerProvider;
import ghidra.app.plugin.core.debug.gui.action.*;
import ghidra.app.plugin.core.debug.gui.breakpoint.DebuggerBreakpointsProvider;
import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider;
import ghidra.app.plugin.core.debug.gui.copying.DebuggerCopyActionsPlugin;
import ghidra.app.plugin.core.debug.gui.copying.DebuggerCopyIntoProgramDialog;
import ghidra.app.plugin.core.debug.gui.diff.DebuggerTraceViewDiffPlugin;
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingProvider;
import ghidra.app.plugin.core.debug.gui.memory.DebuggerMemoryBytesProvider;
import ghidra.app.plugin.core.debug.gui.memory.DebuggerRegionsProvider;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerStaticMappingProvider;
import ghidra.app.plugin.core.debug.gui.pcode.DebuggerPcodeStepperPlugin;
import ghidra.app.plugin.core.debug.gui.pcode.DebuggerPcodeStepperProvider;
import ghidra.app.plugin.core.debug.gui.register.DebuggerRegistersProvider;
import ghidra.app.plugin.core.debug.gui.stack.DebuggerStackProvider;
import ghidra.app.plugin.core.debug.gui.stack.vars.VariableValueHoverPlugin;
import ghidra.app.plugin.core.debug.gui.target.DebuggerTargetsPlugin;
import ghidra.app.plugin.core.debug.gui.thread.DebuggerThreadsProvider;
import ghidra.app.plugin.core.debug.gui.time.DebuggerTimeProvider;
import ghidra.app.plugin.core.debug.gui.time.DebuggerTimeSelectionDialog;
import ghidra.app.plugin.core.debug.gui.watch.DebuggerWatchesProvider;
import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePlugin;
import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePlugin.EmulateProgramAction;
import ghidra.app.plugin.core.debug.service.model.DebuggerConnectDialog;
import ghidra.app.plugin.core.debug.stack.StackUnwinderTest;
import ghidra.app.plugin.core.debug.stack.StackUnwinderTest.HoverLocation;
import ghidra.app.plugin.core.debug.stack.UnwindStackCommand;
import ghidra.app.plugin.core.debug.workflow.*;
import ghidra.app.plugin.core.decompile.DecompilerProvider;
import ghidra.app.script.GhidraState;
import ghidra.app.services.*;
import ghidra.app.services.DebuggerEmulationService.EmulationResult;
import ghidra.app.util.importer.AutoImporter;
import ghidra.app.util.importer.MessageLog;
import ghidra.app.util.opinion.LoadResults;
import ghidra.async.AsyncTestUtils;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.target.TargetLauncher;
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
import ghidra.dbg.testutil.DummyProc;
import ghidra.dbg.util.ConfigurableFactory.Property;
import ghidra.debug.api.model.DebuggerProgramLaunchOffer;
import ghidra.debug.api.model.DebuggerProgramLaunchOffer.*;
import ghidra.debug.api.watch.WatchRow;
import ghidra.debug.api.workflow.DebuggerBot;
import ghidra.debug.flatapi.FlatDebuggerAPI;
import ghidra.framework.Application;
import ghidra.framework.TestApplicationUtils;
import ghidra.framework.model.DomainFolder;
import ghidra.framework.model.DomainObject;
import ghidra.framework.plugintool.util.PluginUtils;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.data.*;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.util.GhidraProgramUtilities;
import ghidra.program.util.ProgramSelection;
import ghidra.test.TestEnv;
import ghidra.trace.model.breakpoint.TraceBreakpoint;
import ghidra.trace.model.guest.TracePlatform;
import ghidra.trace.model.modules.TraceModule;
import ghidra.trace.model.modules.TraceSection;
import ghidra.trace.model.program.TraceProgramView;
import ghidra.trace.model.time.schedule.*;
import ghidra.util.InvalidNameException;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.ConsoleTaskMonitor;
import help.screenshot.GhidraScreenShotGenerator;
public class TutorialDebuggerScreenShots extends GhidraScreenShotGenerator
implements AsyncTestUtils {
protected static final String TUTORIAL_PATH =
TestApplicationUtils.getInstallationDirectory() + "/GhidraDocs/GhidraClass/Debugger/";
protected static final File TUTORIAL_DIR = new File(TUTORIAL_PATH);
protected static final String TERMMINES_PATH = "/tmp/termmines";
protected final ConsoleTaskMonitor monitor = new ConsoleTaskMonitor();
protected ProgramManager programManager;
protected CodeViewerService staticListingService;
protected DebuggerWorkflowService workflowService;
protected DebuggerModelService modelService;
protected DebuggerModelFactory gdbFactory;
protected final FlatDebuggerAPI flatDbg = new FlatDebuggerAPI() {
@Override
public GhidraState getState() {
Navigatable nav = staticListingService.getNavigatable();
return new GhidraState(tool, env.getProject(),
nav.getProgram(), nav.getLocation(), nav.getSelection(), nav.getHighlight());
}
};
@Override
protected TestEnv newTestEnv() throws Exception {
return new TestEnv("DebuggerCourse");
}
// TODO: Propose this replace waitForProgram
public static void waitForDomainObject(DomainObject object) {
object.flushEvents();
waitForSwing();
}
protected void intoProject(DomainObject obj) {
waitForDomainObject(obj);
DomainFolder rootFolder = tool.getProject()
.getProjectData()
.getRootFolder();
waitForCondition(() -> {
try {
rootFolder.createFile(obj.getName(), obj, monitor);
return true;
}
catch (InvalidNameException | CancelledException e) {
throw new AssertionError(e);
}
catch (IOException e) {
// Usually "object is busy". Try again.
return false;
}
});
}
@Override
public void prepareTool() {
tool = env.launchTool("Debugger");
}
@Override
public void loadProgram() throws Exception {
loadProgram("termmines");
try (Transaction tx = program.openTransaction("Set exe path")) {
program.setExecutablePath(TERMMINES_PATH);
}
intoProject(program);
}
@Override
public void saveOrDisplayImage(String name) {
if (TUTORIAL_DIR.exists()) {
TUTORIAL_DIR.mkdirs();
}
name = name.substring("test".length());
finished(TUTORIAL_DIR, name + ".png");
}
protected void enableBot(Class<? extends DebuggerBot> botCls) {
Set<DebuggerBot> bots = workflowService.getAllBots()
.stream()
.filter(b -> botCls.isInstance(b))
.map(b -> botCls.cast(b))
.collect(Collectors.toSet());
workflowService.enableBots(bots);
}
protected CodeViewerService getStaticListingService() {
for (CodeViewerService viewer : tool.getServices(CodeViewerService.class)) {
if (viewer instanceof DebuggerListingService) {
continue;
}
return viewer;
}
return null;
}
@Before
public void setUpDebugger() throws Throwable {
ResourceFile termminesRsrc = Application.getModuleDataFile("TestResources", "termmines");
File termmines = new File(TERMMINES_PATH);
try {
Files.copy(termminesRsrc.getFile(false).toPath(), termmines.toPath());
}
catch (FileNotFoundException e) {
Msg.warn(this, "Could not update " + TERMMINES_PATH);
}
catch (FileAlreadyExistsException e) {
Files.delete(termmines.toPath());
Files.copy(termminesRsrc.getFile(false).toPath(), termmines.toPath());
}
termmines.setExecutable(true);
programManager = tool.getService(ProgramManager.class);
staticListingService = getStaticListingService();
// This is a front-end plugin. We have to configure it.
workflowService = tool.getService(DebuggerWorkflowService.class);
enableBot(MapModulesDebuggerBot.class);
enableBot(DisassembleAtPcDebuggerBot.class);
modelService = tool.getService(DebuggerModelService.class);
gdbFactory = modelService.getModelFactories()
.stream()
.filter(f -> "gdb".equals(f.getBrief()))
.findAny()
.get();
@SuppressWarnings("unchecked")
Property<String> gdbPathProp =
(Property<String>) gdbFactory.getOptions().get("GDB launch command");
gdbPathProp.setValue(DummyProc.which("gdb"));
}
@Test
public void testGettingStarted_ToolWSpecimen() {
captureToolWindow(1920, 1080);
}
protected void launchProgramInGdb(String extraArgs) throws Throwable {
DebuggerProgramLaunchOffer offer = modelService.getProgramLaunchOffers(program)
.filter(o -> "IN-VM GDB".equals(o.getConfigName()))
.findFirst()
.get();
LaunchConfigurator config = new LaunchConfigurator() {
@Override
public Map<String, ?> configureLauncher(TargetLauncher launcher,
Map<String, ?> arguments, RelPrompt relPrompt) {
if (extraArgs.length() == 0) {
return arguments;
}
Map<String, Object> adjusted = new HashMap<>(arguments);
TargetCmdLineLauncher.PARAMETER_CMDLINE_ARGS.adjust(adjusted,
c -> c + " " + extraArgs);
return adjusted;
}
};
LaunchResult result = waitOn(offer.launchProgram(monitor, PromptMode.NEVER, config));
if (result.exception() != null) {
throw result.exception();
}
}
protected void launchProgramInGdb() throws Throwable {
launchProgramInGdb("");
}
@Test
public void testGettingStarted_DisassemblyAfterLaunch() throws Throwable {
launchProgramInGdb();
captureToolWindow(1920, 1080);
}
@Test
public void testBreakpoints_EmptyAfterLaunch() throws Throwable {
launchProgramInGdb();
tool.setSize(1920, 1080);
captureProvider(DebuggerBreakpointsProvider.class);
}
protected void placeBreakpointsSRandRand() throws Throwable {
assertTrue(flatDbg.execute("break srand"));
assertTrue(flatDbg.execute("break rand"));
waitForCondition(() -> flatDbg.getAllBreakpoints().size() == 2);
}
protected void placeBreakpointsRand() throws Throwable {
assertTrue(flatDbg.execute("break rand"));
waitForCondition(() -> flatDbg.getAllBreakpoints().size() == 1);
}
@Test
public void testBreakpoints_PopAfterSRandRand() throws Throwable {
launchProgramInGdb();
placeBreakpointsSRandRand();
tool.setSize(1920, 1080);
captureProvider(DebuggerBreakpointsProvider.class);
}
protected Address navigateToBreakpoint(String comment) {
TraceBreakpoint bp = flatDbg.getAllBreakpoints()
.stream()
.flatMap(l -> l.getTraceBreakpoints().stream())
.filter(l -> comment.equals(l.getComment()))
.findAny()
.get();
Address dynAddr = bp.getMinAddress();
flatDbg.goToDynamic(dynAddr);
return dynAddr;
}
@Test
public void testBreakpoints_MissingModuleNote() throws Throwable {
launchProgramInGdb();
placeBreakpointsSRandRand();
navigateToBreakpoint("srand");
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerConsoleProvider.class);
}
protected Program importModule(TraceModule module) throws Throwable {
Program prog = null;
try {
MessageLog log = new MessageLog();
LoadResults<Program> result = AutoImporter.importByUsingBestGuess(
new File(module.getName()), env.getProject(), "/", this, log, monitor);
result.save(env.getProject(), this, log, monitor);
prog = result.getPrimaryDomainObject();
GhidraProgramUtilities.markProgramNotToAskToAnalyze(prog);
programManager.openProgram(prog);
}
finally {
if (prog != null) {
prog.release(this);
}
}
return prog;
}
protected void analyze(Program prog) {
DockingActionIf actAutoAnalyze = Unique.assertOne(getActionsByOwnerAndName(tool,
PluginUtils.getPluginNameFromClass(AutoAnalysisPlugin.class), "Auto Analyze"));
performAction(actAutoAnalyze);
}
protected TraceModule getModuleContaining(Address dynAddr) {
return Unique.assertOne(flatDbg.getCurrentTrace()
.getModuleManager()
.getModulesAt(flatDbg.getCurrentSnap(), dynAddr));
}
protected void disassembleSymbol(Program prog, String name) {
for (Symbol sym : prog.getSymbolTable().getLabelOrFunctionSymbols(name, null)) {
tool.executeBackgroundCommand(new DisassembleCommand(sym.getAddress(), null, true),
prog);
}
waitForTasks(600 * 1000);
}
@Test
public void testBreakpoints_SyncedAfterImportLibC() throws Throwable {
launchProgramInGdb();
placeBreakpointsSRandRand();
showProvider(DebuggerBreakpointsProvider.class);
Address dynAddr = navigateToBreakpoint("srand");
TraceModule modLibC = getModuleContaining(dynAddr);
Program progLibC = importModule(modLibC);
waitForCondition(() -> flatDbg.translateDynamicToStatic(dynAddr) != null);
disassembleSymbol(progLibC, "srand");
// Just to be sure.
goTo(tool, progLibC, flatDbg.translateDynamicToStatic(dynAddr));
captureToolWindow(1920, 1080);
}
@Test
public void testBreakpoints_SeedValueAfterBreakSRand() throws Throwable {
addPlugin(tool, VariableValueHoverPlugin.class);
launchProgramInGdb();
placeBreakpointsSRandRand();
showProvider(DecompilerProvider.class);
Address dynAddr = navigateToBreakpoint("srand");
TraceModule modLibC = getModuleContaining(dynAddr);
Program progLibC = importModule(modLibC);
waitForCondition(() -> flatDbg.translateDynamicToStatic(dynAddr) != null);
disassembleSymbol(progLibC, "srand");
Address stAddr = flatDbg.translateDynamicToStatic(dynAddr);
// Just to be sure.
goTo(tool, progLibC, stAddr);
flatDbg.resume();
Function funSRand = progLibC.getFunctionManager().getFunctionAt(stAddr);
runSwing(() -> tool.setSize(1920, 1080));
DecompilerProvider dProvider = waitForComponentProvider(DecompilerProvider.class);
DecompilerPanel dPanel = dProvider.getDecompilerPanel();
HoverLocation loc = StackUnwinderTest.findTokenLocation(dPanel, funSRand, "param_1",
"void srand(ulong param_1)");
runSwing(() -> dPanel.goToToken(loc.token()));
FieldPanel fieldPanel = dPanel.getFieldPanel();
Rectangle rect = fieldPanel.getCursorBounds();
MouseEvent event =
new MouseEvent(fieldPanel, 0, System.currentTimeMillis(), 0, rect.x, rect.y, 0, false);
fieldPanel.getHoverHandler().mouseHovered(event);
waitForSwing();
sleep(500); // Give time for GDB to respond async
captureProviderWithScreenShot(dProvider);
}
@Test
public void testState_ListingAfterCallRand() throws Throwable {
launchProgramInGdb();
placeBreakpointsRand();
flatDbg.resume();
flatDbg.stepOut();
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerListingProvider.class);
}
@Test
public void testState_ListingStackAfterCallRand() throws Throwable {
launchProgramInGdb();
placeBreakpointsRand();
flatDbg.resume();
flatDbg.stepOut();
DebuggerListingService listingService = tool.getService(DebuggerListingService.class);
listingService.setTrackingSpec(SPLocationTrackingSpec.INSTANCE);
sleep(1000);
tool.execute(new UnwindStackCommand(tool, flatDbg.getCurrentDebuggerCoordinates()),
flatDbg.getCurrentTrace());
waitForTasks();
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerListingProvider.class);
}
@Test
public void testState_BytesStackAfterCallRand() throws Throwable {
launchProgramInGdb();
placeBreakpointsRand();
flatDbg.resume();
flatDbg.stepOut();
DebuggerMemoryBytesProvider bytesProvider = showProvider(DebuggerMemoryBytesProvider.class);
bytesProvider.setTrackingSpec(SPLocationTrackingSpec.INSTANCE);
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerMemoryBytesProvider.class);
}
@Test
public void testState_RegistersAfterCallRand() throws Throwable {
launchProgramInGdb();
placeBreakpointsRand();
flatDbg.resume();
flatDbg.stepOut();
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerRegistersProvider.class);
}
@Test
public void testState_WatchesInCallSRand() throws Throwable {
launchProgramInGdb();
placeBreakpointsSRandRand();
flatDbg.resume();
DebuggerWatchesService watchesService = tool.getService(DebuggerWatchesService.class);
watchesService.addWatch("RDI");
WatchRow watchRetPtr = watchesService.addWatch("*:8 RSP");
watchRetPtr.setDataType(
new PointerTypedefBuilder(VoidDataType.dataType, 8, null).addressSpace("ram").build());
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerWatchesProvider.class);
}
@Test
public void testNavigation_ThreadsInCallRand() throws Throwable {
launchProgramInGdb();
placeBreakpointsRand();
flatDbg.resume();
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerThreadsProvider.class);
}
@Test
public void testNavigation_StackInCallRand() throws Throwable {
launchProgramInGdb();
placeBreakpointsRand();
Address dynAddr = navigateToBreakpoint("rand");
TraceModule modLibC = getModuleContaining(dynAddr);
importModule(modLibC);
waitForCondition(() -> flatDbg.translateDynamicToStatic(dynAddr) != null);
flatDbg.resume();
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerStackProvider.class);
}
@Test
public void testNavigation_TimeAfterCallSRandCallRand() throws Throwable {
launchProgramInGdb();
placeBreakpointsSRandRand();
flatDbg.resume(); // srand
flatDbg.resume(); // rand.1
flatDbg.stepOut();
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerTimeProvider.class);
}
@Test
public void testNavigation_DialogCompareTimes() throws Throwable {
launchProgramInGdb(); // main
placeBreakpointsRand();
Address pc = flatDbg.getProgramCounter();
long snapA = flatDbg.getCurrentSnap();
TraceModule modTermmines = Unique.assertOne(flatDbg.getCurrentTrace()
.getModuleManager()
.getModulesAt(snapA, pc));
TraceSection secTermminesData = modTermmines.getSectionByName(".data");
flatDbg.readMemory(secTermminesData.getStart(),
(int) secTermminesData.getRange().getLength(), monitor);
flatDbg.resume(); // rand.1
flatDbg.readMemory(secTermminesData.getStart(),
(int) secTermminesData.getRange().getLength(), monitor);
performAction("Compare",
PluginUtils.getPluginNameFromClass(DebuggerTraceViewDiffPlugin.class), false);
DebuggerTimeSelectionDialog timeDialog =
waitForDialogComponent(DebuggerTimeSelectionDialog.class);
timeDialog.setScheduleText(TraceSchedule.snap(snapA).toString());
captureDialog(timeDialog);
}
@Test
public void testNavigation_CompareTimes() throws Throwable {
launchProgramInGdb("-M 15"); // main
placeBreakpointsRand();
Address pc = flatDbg.getProgramCounter();
long snapA = flatDbg.getCurrentSnap();
TraceModule modTermmines = Unique.assertOne(flatDbg.getCurrentTrace()
.getModuleManager()
.getModulesAt(snapA, pc));
TraceSection secTermminesData = modTermmines.getSectionByName(".data");
flatDbg.readMemory(secTermminesData.getStart(),
(int) secTermminesData.getRange().getLength(), monitor);
flatDbg.resume(); // rand.1
flatDbg.waitForBreak(1000, TimeUnit.MILLISECONDS);
flatDbg.readMemory(secTermminesData.getStart(),
(int) secTermminesData.getRange().getLength(), monitor);
performAction("Compare",
PluginUtils.getPluginNameFromClass(DebuggerTraceViewDiffPlugin.class), false);
DebuggerTimeSelectionDialog timeDialog =
waitForDialogComponent(DebuggerTimeSelectionDialog.class);
runSwing(() -> timeDialog.setScheduleText(TraceSchedule.snap(snapA).toString()));
runSwing(() -> timeDialog.okCallback());
DockingActionIf actionNextDiff = waitForValue(() -> {
try {
return Unique.assertOne(getActionsByOwnerAndName(tool,
PluginUtils.getPluginNameFromClass(DebuggerTraceViewDiffPlugin.class),
"Next Difference"));
}
catch (Throwable e) {
return null;
}
});
waitForCondition(() -> actionNextDiff.isEnabled());
flatDbg.goToDynamic(secTermminesData.getStart());
performAction(actionNextDiff);
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerListingProvider.class);
}
@Test
public void testMemoryMap_RegionsAfterLaunch() throws Throwable {
launchProgramInGdb();
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerRegionsProvider.class);
}
@Test
public void testMemoryMap_ModulesAfterLaunch() throws Throwable {
launchProgramInGdb();
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerModulesProvider.class);
}
@Test
public void testMemoryMap_StaticMappingAfterLaunch() throws Throwable {
launchProgramInGdb();
placeBreakpointsSRandRand();
showProvider(DebuggerStaticMappingProvider.class);
Address dynAddr = navigateToBreakpoint("srand");
TraceModule modLibC = getModuleContaining(dynAddr);
importModule(modLibC);
waitForCondition(() -> flatDbg.translateDynamicToStatic(dynAddr) != null);
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerStaticMappingProvider.class);
}
@Test
public void testMemoryMap_CopyNcursesInto() throws Throwable {
launchProgramInGdb();
TraceModule modNcurses = flatDbg.getCurrentTrace()
.getModuleManager()
.getAllModules()
.stream()
.filter(m -> m.getName().contains("ncurses"))
.findAny()
.get();
DebuggerListingService listings = tool.getService(DebuggerListingService.class);
runSwing(() -> listings
.setCurrentSelection(new ProgramSelection(new AddressSet(modNcurses.getRange()))));
performAction("Copy Into New Program",
PluginUtils.getPluginNameFromClass(DebuggerCopyActionsPlugin.class), false);
captureDialog(DebuggerCopyIntoProgramDialog.class);
}
@Test
public void testRemoteTargets_GdbOverSsh() throws Throwable {
performAction("Connect", PluginUtils.getPluginNameFromClass(DebuggerTargetsPlugin.class),
false);
DebuggerConnectDialog dialog = waitForDialogComponent(DebuggerConnectDialog.class);
runSwing(() -> dialog.setFactoryByBrief("gdb via SSH"));
captureDialog(dialog);
}
@Test
public void testRemoteTargets_Gadp() throws Throwable {
performAction("Connect", PluginUtils.getPluginNameFromClass(DebuggerTargetsPlugin.class),
false);
DebuggerConnectDialog dialog = waitForDialogComponent(DebuggerConnectDialog.class);
runSwing(() -> dialog.setFactoryByBrief("Ghidra debug agent (GADP)"));
captureDialog(dialog);
}
protected Function findCommandLineParser() throws Throwable {
for (Data data : program.getListing().getDefinedData(true)) {
Object value = data.getValue();
if (!(value instanceof String str) || !str.startsWith("Usage: ")) {
continue;
}
for (Reference refToUsage : data.getReferenceIteratorTo()) {
Address from = refToUsage.getFromAddress();
Function function = program.getFunctionManager().getFunctionContaining(from);
if (function != null) {
return function;
}
}
}
throw new AssertionError("Cannot find command-line parsing function");
}
protected CodeViewerProvider getCodeViewerProvider() {
return (CodeViewerProvider) staticListingService.getNavigatable(); // HACK
}
protected void goToStaticUntilContext(Address address) {
CodeViewerProvider provider = getCodeViewerProvider();
waitForCondition(() -> {
goTo(tool, program, address);
runSwing(() -> provider.contextChanged());
return provider.getActionContext(null) instanceof ProgramLocationActionContext;
});
}
protected void emulateCommandLineParser() throws Throwable {
Function function = findCommandLineParser();
goToStaticUntilContext(function.getEntryPoint());
performAction(EmulateProgramAction.NAME,
PluginUtils.getPluginNameFromClass(DebuggerEmulationServicePlugin.class),
getCodeViewerProvider(), true);
}
@Test
public void testEmulation_LazyStaleListing() throws Throwable {
emulateCommandLineParser();
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerListingProvider.class);
}
@Test
public void testEmulation_ListingAfterResume() throws Throwable {
emulateCommandLineParser();
DebuggerListingProvider listing = getProvider(DebuggerListingProvider.class);
listing.setAutoReadMemorySpec(
AutoReadMemorySpec.fromConfigName(LoadEmulatorAutoReadMemorySpec.CONFIG_NAME));
EmulationResult result = flatDbg.getEmulationService()
.run(flatDbg.getCurrentPlatform(), flatDbg.getCurrentEmulationSchedule(), monitor,
Scheduler.oneThread(flatDbg.getCurrentThread()));
flatDbg.getTraceManager().activateTime(result.schedule());
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerListingProvider.class);
}
protected void addWatchesForCmdline() throws Throwable {
DebuggerWatchesService watchesService = tool.getService(DebuggerWatchesService.class);
watchesService.addWatch("RSP");
watchesService.addWatch("RDI");
watchesService.addWatch("RSI");
watchesService.addWatch("*:8 (RSI + 0)");
watchesService.addWatch("*:8 (RSI + 8)");
watchesService.addWatch("*:8 (RSI + 16)");
watchesService.addWatch("*:30 (*:8 (RSI + 0))")
.setDataType(TerminatedStringDataType.dataType);
watchesService.addWatch("*:30 (*:8 (RSI + 8))")
.setDataType(TerminatedStringDataType.dataType);
watchesService.addWatch("*:30 (*:8 (RSI + 16))")
.setDataType(TerminatedStringDataType.dataType);
}
@Test
public void testEmulation_WatchesForCmdline() throws Throwable {
emulateCommandLineParser();
addWatchesForCmdline();
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerWatchesProvider.class);
}
protected void activateCmdlinePatchedSchedule() throws Throwable {
TracePlatform platform = flatDbg.getCurrentPlatform();
Address forArgv0 = platform.getAddressFactory().getAddress("00001018");
Address forArgv1 = forArgv0.add("termmines\0".length());
Address forArgv2 = forArgv1.add("-s\0".length());
List<String> sleigh = new ArrayList<>();
sleigh.add("RDI=3");
sleigh.add("RSI=0x1000");
sleigh.addAll(PatchStep.generateSleigh(platform.getLanguage(),
forArgv0, "termmines\0".getBytes()));
sleigh.addAll(PatchStep.generateSleigh(platform.getLanguage(),
forArgv1, "-s\0".getBytes()));
sleigh.addAll(PatchStep.generateSleigh(platform.getLanguage(),
forArgv2, "Advanced\0".getBytes()));
sleigh.add("*:8 (RSI + 0) = 0x" + forArgv0);
sleigh.add("*:8 (RSI + 8) = 0x" + forArgv1);
sleigh.add("*:8 (RSI + 16) = 0x" + forArgv2);
TraceSchedule schedule = flatDbg.getCurrentEmulationSchedule();
schedule = schedule.patched(flatDbg.getCurrentThread(), platform.getLanguage(), sleigh);
flatDbg.getTraceManager().activateTime(schedule);
getProvider(DebuggerWatchesProvider.class).waitEvaluate(1000);
}
@Test
public void testEmulation_WatchesForCmdlineSet() throws Throwable {
emulateCommandLineParser();
addWatchesForCmdline();
activateCmdlinePatchedSchedule();
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerWatchesProvider.class);
}
@Test
public void testEmulation_ListingForCmdlineSet() throws Throwable {
emulateCommandLineParser();
activateCmdlinePatchedSchedule();
Address addrArgv = flatDbg.getCurrentPlatform().getAddressFactory().getAddress("00001000");
TraceProgramView view = flatDbg.getCurrentView();
waitForCondition(() -> view.getSnap() != 0);
try (Transaction tx = view.openTransaction("Place units")) {
Listing listing = view.getListing();
Data datArgv =
listing.createData(addrArgv, new ArrayDataType(PointerDataType.dataType, 3, 8));
Address forArgv0 = (Address) datArgv.getComponent(0).getValue();
Address forArgv1 = (Address) datArgv.getComponent(1).getValue();
Address forArgv2 = (Address) datArgv.getComponent(2).getValue();
listing.createData(forArgv0, TerminatedStringDataType.dataType);
listing.createData(forArgv1, TerminatedStringDataType.dataType);
listing.createData(forArgv2, TerminatedStringDataType.dataType);
}
flatDbg.goToDynamic("00001010");
runSwing(() -> tool.setSize(1920, 1080));
captureProvider(DebuggerListingProvider.class);
}
@Test
public void testEmulation_PcodeStepper() throws Throwable {
runSwing(() -> tool.setSize(1920, 1080));
addPlugin(tool, DebuggerPcodeStepperPlugin.class);
emulateCommandLineParser();
flatDbg.stepEmuPcodeOp(1, monitor);
captureProvider(DebuggerPcodeStepperProvider.class);
}
}

View file

@ -1,405 +0,0 @@
/* ###
* 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.rmi;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
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 ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin;
import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
import ghidra.app.services.TraceRmiService;
import ghidra.dbg.testutil.DummyProc;
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.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.Msg;
import ghidra.util.NumericUtilities;
public abstract class AbstractDbgEngTraceRmiTest extends AbstractGhidraHeadedDebuggerGUITest {
/**
* 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 ghidradbg.commands 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;
protected TraceRmiService traceRmi;
private Path pythonPath;
private Path outFile;
private Path errFile;
@Before
public void assertOS() {
assumeTrue(OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS);
}
//@BeforeClass
public static void setupPython() throws Throwable {
new ProcessBuilder("gradle", "Debugger-agent-dbgeng:assemblePyPackage")
.directory(TestApplicationUtils.getInstallationDirectory())
.inheritIO()
.start()
.waitFor();
}
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-dbgeng",
"build/pypkg/src").getAbsolutePath();
String add = rmiPyPkg + sep + gdbPyPkg;
pb.environment().compute("PYTHONPATH", (k, v) -> v == null ? add : (v + sep + add));
}
@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"));
}
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);
}
return stdout;
}
}
protected record ExecInPython(Process python, CompletableFuture<PythonResult> future) {
}
@SuppressWarnings("resource") // Do not close stdin
protected ExecInPython execInPython(String script) throws IOException {
ProcessBuilder pb = new ProcessBuilder(pythonPath.toString(), "-i");
setPythonPath(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);
pb.redirectOutput(outFile.toFile());
pb.redirectError(errFile.toFile());
Process pyproc = pb.start();
OutputStream stdin = pyproc.getOutputStream();
stdin.write(script.getBytes());
stdin.flush();
//stdin.close();
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");
execute.invoke(Map.of("cmd", cmd));
}
public RemoteAsyncResult executeAsync(String cmd) {
RemoteMethod execute = getMethod("execute");
return execute.invokeAsync(Map.of("cmd", cmd));
}
public String executeCapture(String expr) {
RemoteMethod execute = getMethod("evaluate");
return (String) execute.invoke(Map.of("expr", expr));
}
@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(() -> assertTrue(connection.isClosed()));
}
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(() -> assertTrue(conn.connection.isClosed()));
return stdout;
}
protected void waitStopped() {
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]", Lifespan.at(0)));
waitForPass(() -> assertEquals("STOPPED", tb.objValue(proc, 0, "_state")));
waitTxDone();
}
protected void waitRunning() {
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]", Lifespan.at(0)));
waitForPass(() -> assertEquals("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, String key, Address addr, int len,
Set<TraceBreakpointKind> kinds, String expression) throws Exception {
assertEquals(key, locVal.getEntryKey());
TraceObject loc = locVal.getChild();
TraceObject spec = loc;
assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue());
assertEquals(TraceBreakpointKindSet.encode(kinds), spec.getValue(0, "_kinds").getValue());
assertTrue(spec.getValue(0, "_expression").getValue().toString().contains(expression));
}
protected void assertWatchLoc(TraceObjectValue locVal, String key, Address addr, int len,
Set<TraceBreakpointKind> kinds, String expression) throws Exception {
assertEquals(key, locVal.getEntryKey());
TraceObject loc = locVal.getChild();
assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue());
assertEquals(TraceBreakpointKindSet.encode(kinds), loc.getValue(0, "_kinds").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

@ -1,375 +0,0 @@
/* ###
* 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.rmi;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Objects;
import org.junit.Ignore;
import org.junit.Test;
import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathPredicates;
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.time.TraceSnapshot;
public class DbgEngHooksTest extends AbstractDbgEngTraceRmiTest {
private static final long RUN_TIMEOUT_MS = 20000;
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 {
conn.close();
mdo.close();
}
}
@SuppressWarnings("resource")
protected PythonAndTrace startAndSyncPython(String exec) throws Exception {
PythonAndConnection conn = startAndConnectPython();
try {
ManagedDomainObject mdo;
conn.execute("from ghidradbg.commands import *");
conn.execute(
"util.set_convenience_variable('ghidra-language', 'x86:LE:64:default')");
if (exec != null) {
start(conn, exec);
mdo = waitDomainObject("/New Traces/pydbg/" + exec);
}
else {
conn.execute("ghidra_trace_start()");
mdo = waitDomainObject("/New Traces/pydbg/noname");
}
tb = new ToyDBTraceBuilder((Trace) mdo.get());
return new PythonAndTrace(conn, mdo);
}
catch (Exception e) {
conn.close();
throw e;
}
}
protected long lastSnap(PythonAndTrace conn) {
return conn.conn.connection().getLastSnapshot(tb.trace);
}
@Test // The 10s wait makes this a pretty expensive test
public void testOnNewThread() throws Exception {
try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) {
conn.execute("from ghidradbg.commands import *");
txPut(conn, "processes");
waitForPass(() -> {
TraceObject proc = tb.objAny("Processes[]");
assertNotNull(proc);
assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
txPut(conn, "threads");
waitForPass(() -> assertEquals(4,
tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()),
RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("dbg().go(10)");
waitForPass(
() -> assertTrue(tb.objValues(lastSnap(conn), "Processes[].Threads[]").size() > 4),
RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnThreadSelected() throws Exception {
try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) {
txPut(conn, "processes");
waitForPass(() -> {
TraceObject inf = tb.objAny("Processes[]");
assertNotNull(inf);
assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
txPut(conn, "threads");
waitForPass(() -> assertEquals(4,
tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()),
RUN_TIMEOUT_MS, RETRY_MS);
// Now the real test
conn.execute("util.select_thread(1)");
waitForPass(() -> {
String tnum = conn.executeCapture("util.selected_thread()");
assertTrue(tnum.contains("1"));
String threadIndex = threadIndex(traceManager.getCurrentObject());
assertTrue(tnum.contains(threadIndex));
}, RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("util.select_thread(2)");
waitForPass(() -> {
String tnum = conn.executeCapture("util.selected_thread()");
assertTrue(tnum.contains("2"));
String threadIndex = threadIndex(traceManager.getCurrentObject());
assertTrue(tnum.contains(threadIndex));
}, RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("util.select_thread(0)");
waitForPass(() -> {
String tnum = conn.executeCapture("util.selected_thread()");
assertTrue(tnum.contains("0"));
String threadIndex = threadIndex(traceManager.getCurrentObject());
assertTrue(tnum.contains(threadIndex));
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
protected String getIndex(TraceObject object, String pattern, int n) {
if (object == null) {
return null;
}
PathPattern pat = PathPredicates.parse(pattern).getSingletonPattern();
// if (pat.countWildcards() != 1) {
// throw new IllegalArgumentException("Exactly one wildcard required");
// }
List<String> path = object.getCanonicalPath().getKeyList();
if (path.size() < pat.asPath().size()) {
return null;
}
List<String> matched = pat.matchKeys(path.subList(0, pat.asPath().size()));
if (matched == null) {
return null;
}
if (matched.size() <= n) {
return null;
}
return matched.get(n);
}
protected String threadIndex(TraceObject object) {
return getIndex(object, "Processes[].Threads[]", 1);
}
protected String frameIndex(TraceObject object) {
return getIndex(object, "Processes[].Threads[].Stack[]", 2);
}
@Test
@Ignore
public void testOnSyscallMemory() throws Exception {
// TODO: Need a specimen
// FWIW, I've already seen this getting exercised in other tests.
}
//@Test - dbgeng has limited support via DEBUG_CDS_DATA,
// but expensive to implement anything here
public void testOnMemoryChanged() throws Exception {
try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) {
conn.execute("ghidra_trace_txstart('Tx')");
conn.execute("ghidra_trace_putmem('$pc 10')");
conn.execute("ghidra_trace_txcommit()");
long address = getAddressAtOffset(conn, 0);
conn.execute("util.get_debugger().write(" + address + ", b'\\x7f')");
waitForPass(() -> {
ByteBuffer buf = ByteBuffer.allocate(10);
tb.trace.getMemoryManager().getBytes(lastSnap(conn), tb.addr(address), buf);
assertEquals(0x7f, buf.get(0));
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnRegisterChanged() throws Exception {
try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) {
conn.execute("ghidra_trace_txstart('Tx')");
conn.execute("ghidra_trace_putreg()");
conn.execute("ghidra_trace_txcommit()");
conn.execute("util.get_debugger().reg._set_register('rax', 0x1234)");
conn.execute("util.get_debugger().stepi()");
String path = "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.exe")) {
txPut(conn, "processes");
conn.execute("util.get_debugger()._control.SetExecutionStatus(DbgEng.DEBUG_STATUS_GO)");
waitRunning();
TraceObject proc = waitForValue(() -> tb.objAny("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.exe")) {
txPut(conn, "processes");
TraceObject proc = waitForValue(() -> tb.objAny("Processes[]"));
waitForPass(() -> {
assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnExited() throws Exception {
try (PythonAndTrace conn = startAndSyncPython("netstat.exe")) {
txPut(conn, "processes");
waitStopped();
conn.execute("util.get_debugger().go()");
waitForPass(() -> {
TraceSnapshot snapshot =
tb.trace.getTimeManager().getSnapshot(lastSnap(conn), false);
assertNotNull(snapshot);
assertEquals("Exited with code 0", snapshot.getDescription());
TraceObject proc = tb.objAny("Processes[]");
assertNotNull(proc);
Object val = tb.objValue(proc, lastSnap(conn), "_exit_code");
assertThat(val, instanceOf(Number.class));
assertEquals(0, ((Number) val).longValue());
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnBreakpointCreated() throws Exception {
try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) {
txPut(conn, "breakpoints");
assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size());
conn.execute("dbg = util.get_debugger()");
conn.execute("pc = dbg.reg.get_pc()");
conn.execute("dbg.bp(expr=pc)");
waitForPass(() -> {
List<Object> brks = tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]");
assertEquals(1, brks.size());
return (TraceObject) brks.get(0);
});
}
}
@Test
public void testOnBreakpointModified() throws Exception {
try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) {
txPut(conn, "breakpoints");
assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size());
conn.execute("dbg = util.get_debugger()");
conn.execute("pc = dbg.reg.get_pc()");
conn.execute("dbg.bp(expr=pc)");
TraceObject brk = waitForPass(() -> {
List<Object> brks = tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]");
assertEquals(1, brks.size());
return (TraceObject) brks.get(0);
});
assertEquals(true, tb.objValue(brk, lastSnap(conn), "Enabled"));
conn.execute("dbg.bd(0)");
conn.execute("dbg.stepi()");
assertEquals(false, tb.objValue(brk, lastSnap(conn), "Enabled"));
/* Not currently enabled
assertEquals("", tb.objValue(brk, lastSnap(conn), "Command"));
conn.execute("dbg.bp(expr=pc, windbgcmd='bl')");
conn.execute("dbg.stepi()");
assertEquals("bl", tb.objValue(brk, lastSnap(conn), "Command"));
*/
}
}
@Test
public void testOnBreakpointDeleted() throws Exception {
try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) {
txPut(conn, "breakpoints");
assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size());
conn.execute("dbg = util.get_debugger()");
conn.execute("pc = dbg.reg.get_pc()");
conn.execute("dbg.bp(expr=pc)");
TraceObject brk = waitForPass(() -> {
List<Object> brks = tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]");
assertEquals(1, brks.size());
return (TraceObject) brks.get(0);
});
conn.execute("dbg.cmd('bc %s')".formatted(brk.getCanonicalPath().index()));
conn.execute("dbg.stepi()");
waitForPass(
() -> assertEquals(0,
tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size()));
}
}
private void start(PythonAndConnection conn, String obj) {
conn.execute("from ghidradbg.commands import *");
if (obj != null)
conn.execute("ghidra_trace_create('" + obj + "')");
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 long getAddressAtOffset(PythonAndTrace conn, int offset) {
String inst = "util.get_inst(util.get_debugger().reg.get_pc()+" + offset + ")";
String ret = conn.executeCapture(inst);
String[] split = ret.split("\\s+"); // get target
return Long.decode(split[1]);
}
}

View file

@ -1,961 +0,0 @@
/* ###
* 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.rmi;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.*;
import java.util.*;
import org.junit.Test;
import generic.Unique;
import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
import ghidra.dbg.testutil.DummyProc;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathPredicates;
import ghidra.debug.api.tracermi.RemoteMethod;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.RegisterValue;
import ghidra.trace.database.ToyDBTraceBuilder;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.breakpoint.TraceBreakpointKind;
import ghidra.trace.model.memory.TraceMemoryRegion;
import ghidra.trace.model.memory.TraceMemorySpace;
import ghidra.trace.model.modules.TraceModule;
import ghidra.trace.model.target.TraceObject;
import ghidra.trace.model.target.TraceObjectValue;
public class DbgEngMethodsTest extends AbstractDbgEngTraceRmiTest {
@Test
public void testEvaluate() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
RemoteMethod evaluate = conn.getMethod("evaluate");
assertEquals("11",
evaluate.invoke(Map.of("expr", "3+4*2")));
}
}
@Test
public void testExecuteCapture() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
RemoteMethod execute = conn.getMethod("execute");
assertEquals(false, execute.parameters().get("to_string").getDefaultValue());
assertEquals("11\n",
execute.invoke(Map.of("cmd", "print(3+4*2)", "to_string", true)));
}
}
@Test
public void testExecute() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
conn.execute("ghidra_trace_kill()");
}
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
// Just confirm it's present
}
}
@Test
public void testRefreshAvailable() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, null);
txCreate(conn, "Available");
RemoteMethod refreshAvailable = conn.getMethod("refresh_available");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
TraceObject available = Objects.requireNonNull(tb.objAny("Available"));
refreshAvailable.invoke(Map.of("node", available));
// Would be nice to control / validate the specifics
List<TraceObject> list = tb.trace.getObjectManager()
.getValuePaths(Lifespan.at(0), PathPredicates.parse("Available[]"))
.map(p -> p.getDestination(null))
.toList();
assertThat(list.size(), greaterThan(2));
}
}
}
@Test
public void testRefreshBreakpoints() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod refreshBreakpoints = conn.getMethod("refresh_breakpoints");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
conn.execute("dbg = util.get_debugger()");
conn.execute("pc = dbg.reg.get_pc()");
conn.execute("dbg.bp(expr=pc)");
conn.execute("dbg.ba(expr=pc+4)");
txPut(conn, "breakpoints");
TraceObject breakpoints =
Objects.requireNonNull(tb.objAny("Processes[].Breakpoints"));
refreshBreakpoints.invoke(Map.of("node", breakpoints));
List<TraceObjectValue> procBreakLocVals = tb.trace.getObjectManager()
.getValuePaths(Lifespan.at(0),
PathPredicates.parse("Processes[].Breakpoints[]"))
.map(p -> p.getLastEntry())
.toList();
assertEquals(2, procBreakLocVals.size());
AddressRange rangeMain =
procBreakLocVals.get(0).getChild().getValue(0, "_range").castValue();
Address main = rangeMain.getMinAddress();
assertBreakLoc(procBreakLocVals.get(0), "[0]", main, 1,
Set.of(TraceBreakpointKind.SW_EXECUTE),
"ntdll!LdrInit");
assertBreakLoc(procBreakLocVals.get(1), "[1]", main.add(4), 1,
Set.of(TraceBreakpointKind.HW_EXECUTE),
"ntdll!LdrInit");
}
}
}
@Test
public void testRefreshBreakpoints2() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "all");
RemoteMethod refreshProcWatchpoints = conn.getMethod("refresh_breakpoints");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
conn.execute("dbg = util.get_debugger()");
conn.execute("pc = dbg.reg.get_pc()");
conn.execute("dbg.ba(expr=pc, access=DbgEng.DEBUG_BREAK_EXECUTE)");
conn.execute("dbg.ba(expr=pc+4, access=DbgEng.DEBUG_BREAK_READ)");
conn.execute("dbg.ba(expr=pc+8, access=DbgEng.DEBUG_BREAK_WRITE)");
TraceObject locations =
Objects.requireNonNull(tb.objAny("Processes[].Breakpoints"));
refreshProcWatchpoints.invoke(Map.of("node", locations));
List<TraceObjectValue> procBreakVals = tb.trace.getObjectManager()
.getValuePaths(Lifespan.at(0),
PathPredicates.parse("Processes[].Breakpoints[]"))
.map(p -> p.getLastEntry())
.toList();
assertEquals(3, procBreakVals.size());
AddressRange rangeMain0 =
procBreakVals.get(0).getChild().getValue(0, "_range").castValue();
Address main0 = rangeMain0.getMinAddress();
AddressRange rangeMain1 =
procBreakVals.get(1).getChild().getValue(0, "_range").castValue();
Address main1 = rangeMain1.getMinAddress();
AddressRange rangeMain2 =
procBreakVals.get(2).getChild().getValue(0, "_range").castValue();
Address main2 = rangeMain2.getMinAddress();
assertWatchLoc(procBreakVals.get(0), "[0]", main0, (int) rangeMain0.getLength(),
Set.of(TraceBreakpointKind.HW_EXECUTE),
"main");
assertWatchLoc(procBreakVals.get(1), "[1]", main1, (int) rangeMain1.getLength(),
Set.of(TraceBreakpointKind.WRITE),
"main+4");
assertWatchLoc(procBreakVals.get(2), "[2]", main2, (int) rangeMain1.getLength(),
Set.of(TraceBreakpointKind.READ),
"main+8");
}
}
}
@Test
public void testRefreshProcesses() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, null);
txCreate(conn, "Processes");
txCreate(conn, "Processes[1]");
RemoteMethod refreshProcesses = conn.getMethod("refresh_processes");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
TraceObject processes = Objects.requireNonNull(tb.objAny("Processes"));
refreshProcesses.invoke(Map.of("node", processes));
// Would be nice to control / validate the specifics
List<TraceObject> list = tb.trace.getObjectManager()
.getValuePaths(Lifespan.at(0), PathPredicates.parse("Processes[]"))
.map(p -> p.getDestination(null))
.toList();
assertEquals(1, list.size());
}
}
}
@Test
public void testRefreshEnvironment() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
String path = "Processes[].Environment";
start(conn, "notepad.exe");
txPut(conn, "all");
RemoteMethod refreshEnvironment = conn.getMethod("refresh_environment");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
TraceObject env = Objects.requireNonNull(tb.objAny(path));
refreshEnvironment.invoke(Map.of("node", env));
// Assumes pydbg on Windows amd64
assertEquals("pydbg", env.getValue(0, "_debugger").getValue());
assertEquals("x86_64", env.getValue(0, "_arch").getValue());
assertEquals("windows", env.getValue(0, "_os").getValue());
assertEquals("little", env.getValue(0, "_endian").getValue());
}
}
}
@Test
public void testRefreshThreads() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
String path = "Processes[].Threads";
start(conn, "notepad.exe");
txCreate(conn, path);
RemoteMethod refreshThreads = conn.getMethod("refresh_threads");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
TraceObject threads = Objects.requireNonNull(tb.objAny(path));
refreshThreads.invoke(Map.of("node", threads));
// Would be nice to control / validate the specifics
int listSize = tb.trace.getThreadManager().getAllThreads().size();
assertEquals(4, listSize);
}
}
}
@Test
public void testRefreshStack() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
String path = "Processes[].Threads[].Stack";
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod refreshStack = conn.getMethod("refresh_stack");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
txPut(conn, "frames");
TraceObject stack = Objects.requireNonNull(tb.objAny(path));
refreshStack.invoke(Map.of("node", stack));
// Would be nice to control / validate the specifics
List<TraceObject> list = tb.trace.getObjectManager()
.getValuePaths(Lifespan.at(0),
PathPredicates.parse("Processes[].Threads[].Stack[]"))
.map(p -> p.getDestination(null))
.toList();
assertTrue(list.size() > 1);
}
}
}
@Test
public void testRefreshRegisters() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
String path = "Processes[].Threads[].Registers";
start(conn, "notepad.exe");
conn.execute("ghidra_trace_txstart('Tx')");
conn.execute("ghidra_trace_putreg()");
conn.execute("ghidra_trace_txcommit()");
RemoteMethod refreshRegisters = conn.getMethod("refresh_registers");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
conn.execute("regs = util.get_debugger().reg");
conn.execute("regs._set_register('rax', int(0xdeadbeef))");
TraceObject registers = Objects.requireNonNull(tb.objAny(path, Lifespan.at(0)));
refreshRegisters.invoke(Map.of("node", registers));
long snap = 0;
AddressSpace t1f0 = tb.trace.getBaseAddressFactory()
.getAddressSpace(registers.getCanonicalPath().toString());
TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(t1f0, false);
RegisterValue rax = regs.getValue(snap, tb.reg("rax"));
assertEquals("deadbeef", rax.getUnsignedValue().toString(16));
}
}
}
@Test
public void testRefreshMappings() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
String path = "Processes[].Memory";
start(conn, "notepad.exe");
txCreate(conn, path);
RemoteMethod refreshMappings = conn.getMethod("refresh_mappings");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
TraceObject memory = Objects.requireNonNull(tb.objAny(path));
refreshMappings.invoke(Map.of("node", memory));
// Would be nice to control / validate the specifics
Collection<? extends TraceMemoryRegion> all =
tb.trace.getMemoryManager().getAllRegions();
assertThat(all.size(), greaterThan(2));
}
}
}
@Test
public void testRefreshModules() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
String path = "Processes[].Modules";
start(conn, "notepad.exe");
txCreate(conn, path);
RemoteMethod refreshModules = conn.getMethod("refresh_modules");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
TraceObject modules = Objects.requireNonNull(tb.objAny(path));
refreshModules.invoke(Map.of("node", modules));
// Would be nice to control / validate the specifics
Collection<? extends TraceModule> all = tb.trace.getModuleManager().getAllModules();
TraceModule modBash =
Unique.assertOne(all.stream().filter(m -> m.getName().contains("notepad.exe")));
assertNotEquals(tb.addr(0), Objects.requireNonNull(modBash.getBase()));
}
}
}
@Test
public void testActivateThread() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod activateThread = conn.getMethod("activate_thread");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
txPut(conn, "threads");
PathPattern pattern =
PathPredicates.parse("Processes[].Threads[]").getSingletonPattern();
List<TraceObject> list = tb.trace.getObjectManager()
.getValuePaths(Lifespan.at(0), pattern)
.map(p -> p.getDestination(null))
.toList();
assertEquals(4, list.size());
for (TraceObject t : list) {
activateThread.invoke(Map.of("thread", t));
String out = conn.executeCapture("util.get_debugger().get_thread()");
List<String> indices = pattern.matchKeys(t.getCanonicalPath().getKeyList());
assertEquals(out, "%s".formatted(indices.get(1)));
}
}
}
}
@Test
public void testRemoveProcess() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "netstat.exe");
txPut(conn, "processes");
RemoteMethod removeProcess = conn.getMethod("remove_process");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/netstat.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
TraceObject proc2 = Objects.requireNonNull(tb.objAny("Processes[]"));
removeProcess.invoke(Map.of("process", proc2));
String out = conn.executeCapture("list(util.process_list())");
assertThat(out, containsString("[]"));
}
}
}
@Test
public void testAttachObj() throws Exception {
try (DummyProc dproc = DummyProc.run("notepad.exe")) {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, null);
txPut(conn, "available");
RemoteMethod attachObj = conn.getMethod("attach_obj");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
TraceObject target =
Objects.requireNonNull(tb.obj("Available[%d]".formatted(dproc.pid)));
attachObj.invoke(Map.of("target", target));
String out = conn.executeCapture("list(util.process_list())");
assertThat(out, containsString("%d".formatted(dproc.pid)));
}
}
}
}
@Test
public void testAttachPid() throws Exception {
try (DummyProc dproc = DummyProc.run("notepad.exe")) {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, null);
txPut(conn, "available");
RemoteMethod attachPid = conn.getMethod("attach_pid");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
Objects.requireNonNull(
tb.objAny("Available[" + dproc.pid + "]", Lifespan.at(0)));
attachPid.invoke(Map.of("pid", dproc.pid));
String out = conn.executeCapture("list(util.process_list())");
assertThat(out, containsString("%d".formatted(dproc.pid)));
}
}
}
}
@Test
public void testDetach() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "netstat.exe");
txPut(conn, "processes");
RemoteMethod detach = conn.getMethod("detach");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/netstat.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]"));
detach.invoke(Map.of("process", proc));
String out = conn.executeCapture("list(util.process_list())");
assertThat(out, containsString("[]"));
}
}
}
@Test
public void testLaunchEntry() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, null);
txPut(conn, "processes");
RemoteMethod launch = conn.getMethod("launch_loader");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
launch.invoke(Map.ofEntries(
Map.entry("file", "notepad.exe")));
String out = conn.executeCapture("list(util.process_list())");
assertThat(out, containsString("notepad.exe"));
}
}
}
@Test //Can't do this test because create(xxx, initial_break=False) doesn't return
public void testLaunch() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, null);
txPut(conn, "processes");
RemoteMethod launch = conn.getMethod("launch");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
launch.invoke(Map.ofEntries(
Map.entry("timeout", 1L),
Map.entry("file", "notepad.exe")));
txPut(conn, "processes");
String out = conn.executeCapture("list(util.process_list())");
assertThat(out, containsString("notepad.exe"));
}
}
}
@Test
public void testKill() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod kill = conn.getMethod("kill");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
waitStopped();
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]"));
kill.invoke(Map.of("process", proc));
String out = conn.executeCapture("list(util.process_list())");
assertThat(out, containsString("[]"));
}
}
}
@Test
public void testStepInto() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod step_into = conn.getMethod("step_into");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
waitStopped();
txPut(conn, "threads");
TraceObject thread = Objects.requireNonNull(tb.objAny("Processes[].Threads[]"));
while (!getInst(conn).contains("call")) {
step_into.invoke(Map.of("thread", thread));
}
String disCall = getInst(conn);
// lab0:
// -> addr0
//
// lab1:
// addr1
String[] split = disCall.split("\\s+"); // get target
long pcCallee = Long.decode(split[split.length - 1]);
step_into.invoke(Map.of("thread", thread));
long pc = getAddressAtOffset(conn, 0);
assertEquals(pcCallee, pc);
}
}
}
@Test
public void testStepOver() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod step_over = conn.getMethod("step_over");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
waitStopped();
txPut(conn, "threads");
TraceObject thread = Objects.requireNonNull(tb.objAny("Processes[].Threads[]"));
while (!getInst(conn).contains("call")) {
step_over.invoke(Map.of("thread", thread));
}
String disCall = getInst(conn);
String[] split = disCall.split("\\s+"); // get target
long pcCallee = Long.decode(split[split.length - 1]);
step_over.invoke(Map.of("thread", thread));
long pc = getAddressAtOffset(conn, 0);
assertNotEquals(pcCallee, pc);
}
}
}
@Test
public void testStepTo() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
RemoteMethod step_into = conn.getMethod("step_into");
RemoteMethod step_to = conn.getMethod("step_to");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
txPut(conn, "threads");
TraceObject thread = Objects.requireNonNull(tb.objAny("Processes[].Threads[]"));
while (!getInst(conn).contains("call")) {
step_into.invoke(Map.of("thread", thread));
}
step_into.invoke(Map.of("thread", thread));
int sz = Integer.parseInt(getInstSizeAtOffset(conn, 0));
for (int i = 0; i < 4; i++) {
sz += Integer.parseInt(getInstSizeAtOffset(conn, sz));
}
long pcNext = getAddressAtOffset(conn, sz);
boolean success = (boolean) step_to
.invoke(Map.of("thread", thread, "address", tb.addr(pcNext), "max", 10));
assertTrue(success);
long pc = getAddressAtOffset(conn, 0);
assertEquals(pcNext, pc);
}
}
}
@Test
public void testStepOut() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod step_into = conn.getMethod("step_into");
RemoteMethod step_out = conn.getMethod("step_out");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
waitStopped();
txPut(conn, "threads");
TraceObject thread = Objects.requireNonNull(tb.objAny("Processes[].Threads[]"));
while (!getInst(conn).contains("call")) {
step_into.invoke(Map.of("thread", thread));
}
int sz = Integer.parseInt(getInstSizeAtOffset(conn, 0));
long pcNext = getAddressAtOffset(conn, sz);
step_into.invoke(Map.of("thread", thread));
step_out.invoke(Map.of("thread", thread));
long pc = getAddressAtOffset(conn, 0);
assertEquals(pcNext, pc);
}
}
}
@Test
public void testBreakAddress() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod breakAddress = conn.getMethod("break_address");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]"));
long address = getAddressAtOffset(conn, 0);
breakAddress.invoke(Map.of("process", proc, "address", tb.addr(address)));
String out = conn.executeCapture("list(util.get_breakpoints())");
assertThat(out, containsString(Long.toHexString(address)));
}
}
}
@Test
public void testBreakExpression() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod breakExpression = conn.getMethod("break_expression");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
waitStopped();
breakExpression.invoke(Map.of("expression", "entry"));
String out = conn.executeCapture("list(util.get_breakpoints())");
assertThat(out, containsString("entry"));
}
}
}
@Test
public void testBreakHardwareAddress() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod breakAddress = conn.getMethod("break_hw_address");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]"));
long address = getAddressAtOffset(conn, 0);
breakAddress.invoke(Map.of("process", proc, "address", tb.addr(address)));
String out = conn.executeCapture("list(util.get_breakpoints())");
assertThat(out, containsString(Long.toHexString(address)));
}
}
}
@Test
public void testBreakHardwareExpression() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod breakExpression = conn.getMethod("break_hw_expression");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
waitStopped();
breakExpression.invoke(Map.of("expression", "entry"));
String out = conn.executeCapture("list(util.get_breakpoints())");
assertThat(out, containsString("entry"));
}
}
}
@Test
public void testBreakReadRange() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod breakRange = conn.getMethod("break_read_range");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
waitStopped();
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]"));
long address = getAddressAtOffset(conn, 0);
AddressRange range = tb.range(address, address + 3); // length 4
breakRange.invoke(Map.of("process", proc, "range", range));
String out = conn.executeCapture("list(util.get_breakpoints())");
assertThat(out, containsString("%x".formatted(address)));
assertThat(out, containsString("sz=4"));
assertThat(out, containsString("type=r"));
}
}
}
@Test
public void testBreakReadExpression() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod breakExpression = conn.getMethod("break_read_expression");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
breakExpression.invoke(Map.of("expression", "ntdll!LdrInitShimEngineDynamic"));
long address = getAddressAtOffset(conn, 0);
String out = conn.executeCapture("list(util.get_breakpoints())");
assertThat(out, containsString(Long.toHexString(address >> 24)));
assertThat(out, containsString("sz=1"));
assertThat(out, containsString("type=r"));
}
}
}
@Test
public void testBreakWriteRange() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod breakRange = conn.getMethod("break_write_range");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
waitStopped();
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]"));
long address = getAddressAtOffset(conn, 0);
AddressRange range = tb.range(address, address + 3); // length 4
breakRange.invoke(Map.of("process", proc, "range", range));
String out = conn.executeCapture("list(util.get_breakpoints())");
assertThat(out, containsString("%x".formatted(address)));
assertThat(out, containsString("sz=4"));
assertThat(out, containsString("type=w"));
}
}
}
@Test
public void testBreakWriteExpression() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod breakExpression = conn.getMethod("break_write_expression");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
breakExpression.invoke(Map.of("expression", "ntdll!LdrInitShimEngineDynamic"));
long address = getAddressAtOffset(conn, 0);
String out = conn.executeCapture("list(util.get_breakpoints())");
assertThat(out, containsString(Long.toHexString(address >> 24)));
assertThat(out, containsString("sz=1"));
assertThat(out, containsString("type=w"));
}
}
}
@Test
public void testBreakAccessRange() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod breakRange = conn.getMethod("break_access_range");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
waitStopped();
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]"));
long address = getAddressAtOffset(conn, 0);
AddressRange range = tb.range(address, address + 3); // length 4
breakRange.invoke(Map.of("process", proc, "range", range));
String out = conn.executeCapture("list(util.get_breakpoints())");
assertThat(out, containsString("%x".formatted(address)));
assertThat(out, containsString("sz=4"));
assertThat(out, containsString("type=rw"));
}
}
}
@Test
public void testBreakAccessExpression() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod breakExpression = conn.getMethod("break_access_expression");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
breakExpression.invoke(Map.of("expression", "ntdll!LdrInitShimEngineDynamic"));
long address = getAddressAtOffset(conn, 0);
String out = conn.executeCapture("list(util.get_breakpoints())");
assertThat(out, containsString(Long.toHexString(address >> 24)));
assertThat(out, containsString("sz=1"));
assertThat(out, containsString("type=rw"));
}
}
}
@Test
public void testToggleBreakpoint() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod breakAddress = conn.getMethod("break_address");
RemoteMethod toggleBreakpoint = conn.getMethod("toggle_breakpoint");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
long address = getAddressAtOffset(conn, 0);
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]"));
breakAddress.invoke(Map.of("process", proc, "address", tb.addr(address)));
txPut(conn, "breakpoints");
TraceObject bpt = Objects.requireNonNull(tb.objAny("Processes[].Breakpoints[]"));
toggleBreakpoint.invoke(Map.of("breakpoint", bpt, "enabled", false));
String out = conn.executeCapture("list(util.get_breakpoints())");
assertThat(out, containsString("disabled"));
}
}
}
@Test
public void testDeleteBreakpoint() throws Exception {
try (PythonAndConnection conn = startAndConnectPython()) {
start(conn, "notepad.exe");
txPut(conn, "processes");
RemoteMethod breakAddress = conn.getMethod("break_address");
RemoteMethod deleteBreakpoint = conn.getMethod("delete_breakpoint");
try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) {
tb = new ToyDBTraceBuilder((Trace) mdo.get());
long address = getAddressAtOffset(conn, 0);
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]"));
breakAddress.invoke(Map.of("process", proc, "address", tb.addr(address)));
txPut(conn, "breakpoints");
TraceObject bpt = Objects.requireNonNull(tb.objAny("Processes[].Breakpoints[]"));
deleteBreakpoint.invoke(Map.of("breakpoint", bpt));
String out = conn.executeCapture("list(util.get_breakpoints())");
assertThat(out, containsString("[]"));
}
}
}
private void start(PythonAndConnection conn, String obj) {
conn.execute("from ghidradbg.commands import *");
if (obj != null)
conn.execute("ghidra_trace_create('" + obj + "')");
else
conn.execute("ghidra_trace_create()");
}
private void txPut(PythonAndConnection conn, String obj) {
conn.execute("ghidra_trace_txstart('Tx')");
conn.execute("ghidra_trace_put_" + obj + "()");
conn.execute("ghidra_trace_txcommit()");
}
private void txCreate(PythonAndConnection conn, String path) {
conn.execute("ghidra_trace_txstart('Fake')");
conn.execute("ghidra_trace_create_obj('%s')".formatted(path));
conn.execute("ghidra_trace_txcommit()");
}
private String getInst(PythonAndConnection conn) {
return getInstAtOffset(conn, 0);
}
private String getInstAtOffset(PythonAndConnection conn, int offset) {
String inst = "util.get_inst(util.get_debugger().reg.get_pc()+" + offset + ")";
String ret = conn.executeCapture(inst);
return ret.substring(1, ret.length() - 1); // remove <>
}
private String getInstSizeAtOffset(PythonAndConnection conn, int offset) {
String instSize = "util.get_inst_sz(util.get_debugger().reg.get_pc()+" + offset + ")";
return conn.executeCapture(instSize);
}
private long getAddressAtOffset(PythonAndConnection conn, int offset) {
String inst = "util.get_inst(util.get_debugger().reg.get_pc()+" + offset + ")";
String ret = conn.executeCapture(inst);
String[] split = ret.split("\\s+"); // get target
return Long.decode(split[1]);
}
}

View file

@ -1,527 +0,0 @@
/* ###
* 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.gdb.rmi;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.*;
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.junit.Before;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin;
import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
import ghidra.app.services.TraceRmiService;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.testutil.DummyProc;
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.trace.model.breakpoint.TraceBreakpointKind;
import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
import ghidra.trace.model.target.*;
import ghidra.util.Msg;
import ghidra.util.NumericUtilities;
public abstract class AbstractGdbTraceRmiTest extends AbstractGhidraHeadedDebuggerGUITest {
/**
* The tests are touchy about anything being printed on stderr, because that's usually where
* Python stack traces go that otherwise are ignored. Unfortunately, GDB also emits some
* warnings there, and these are more common in containers where permissions and resources are
* more restricted. Thus, we add some commands here to disable the features that commonly cause
* these warnings: Leave ASLR alone, and don't try to display source code.
*/
public static final String PREAMBLE = """
set python print-stack full
python import ghidragdb
set disable-randomization off
set source open off
""";
// 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;
public static final String INSTRUMENT_STOPPED = """
ghidra trace tx-open "Fake" 'ghidra trace create-obj Inferiors[1]'
define do-set-stopped
ghidra trace set-value Inferiors[1] _state '"STOPPED"'
end
define set-stopped
ghidra trace tx-open Stopped do-set-stopped
end
python gdb.events.stop.connect(lambda e: gdb.execute("set-stopped"))""";
public static final String INSTRUMENT_RUNNING = """
ghidra trace tx-open "Fake" 'ghidra trace create-obj Inferiors[1]'
define do-set-running
ghidra trace set-value Inferiors[1] _state '"RUNNING"'
end
define set-running
ghidra trace tx-open Running do-set-running
end
python gdb.events.cont.connect(lambda e: gdb.execute("set-running"))""";
protected TraceRmiService traceRmi;
private Path gdbPath;
private Path outFile;
private Path errFile;
// @BeforeClass
public static void setupPython() throws Throwable {
new ProcessBuilder("gradle", "Debugger-agent-gdb:assemblePyPackage")
.directory(TestApplicationUtils.getInstallationDirectory())
.inheritIO()
.start()
.waitFor();
}
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-gdb",
"build/pypkg/src").getAbsolutePath();
String add = rmiPyPkg + sep + gdbPyPkg;
pb.environment().compute("PYTHONPATH", (k, v) -> v == null ? add : (v + sep + add));
}
protected Path getGdbPath() {
return Paths.get(DummyProc.which("gdb"));
}
@Before
public void setupTraceRmi() throws Throwable {
traceRmi = addPlugin(tool, TraceRmiPlugin.class);
gdbPath = getGdbPath();
outFile = Files.createTempFile("gdbout", null);
errFile = Files.createTempFile("gdberr", 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 addrToStringForGdb(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 sockToStringForGdb(SocketAddress address) {
if (address instanceof InetSocketAddress tcp) {
return addrToStringForGdb(tcp.getAddress()) + ":" + tcp.getPort();
}
throw new AssertionError("Unhandled address type " + address);
}
protected record GdbResult(boolean timedOut, int exitCode, String stdout, String stderr) {
protected String handle() {
if (!"".equals(stderr) | 0 != exitCode) {
throw new GdbError(exitCode, stdout, stderr);
}
return stdout;
}
}
protected record ExecInGdb(Process gdb, CompletableFuture<GdbResult> future) {
}
@SuppressWarnings("resource") // Do not close stdin
protected ExecInGdb execInGdb(String script) throws IOException {
ProcessBuilder pb = new ProcessBuilder(gdbPath.toString());
setPythonPath(pb);
// If commands come from file, GDB will quit after EOF.
Msg.info(this, "outFile: " + outFile);
Msg.info(this, "errFile: " + errFile);
pb.redirectInput(ProcessBuilder.Redirect.PIPE);
pb.redirectOutput(outFile.toFile());
pb.redirectError(errFile.toFile());
Process gdbProc = pb.start();
OutputStream stdin = gdbProc.getOutputStream();
stdin.write(script.getBytes());
stdin.flush();
return new ExecInGdb(gdbProc, CompletableFuture.supplyAsync(() -> {
try {
if (!gdbProc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
Msg.error(this, "Timed out waiting for GDB");
gdbProc.destroyForcibly();
gdbProc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS);
return new GdbResult(true, -1, Files.readString(outFile),
Files.readString(errFile));
}
Msg.info(this, "GDB exited with code " + gdbProc.exitValue());
return new GdbResult(false, gdbProc.exitValue(), Files.readString(outFile),
Files.readString(errFile));
}
catch (Exception e) {
return ExceptionUtils.rethrow(e);
}
finally {
gdbProc.destroyForcibly();
}
}));
}
protected static class GdbError extends RuntimeException {
public final int exitCode;
public final String stdout;
public final String stderr;
public GdbError(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<GdbResult> result = execInGdb(script).future;
return result.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle();
}
protected record GdbAndConnection(ExecInGdb 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");
execute.invoke(Map.of("cmd", cmd));
}
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 gdb");
try {
try {
RemoteAsyncResult asyncQuit = executeAsync("quit");
try {
asyncQuit.get(QUIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
}
catch (TimeoutException e) {
/**
* This seems like a bug in gdb. AFAICT, it's a rehash or regression of
* https://sourceware.org/bugzilla/show_bug.cgi?id=17247. If I attach to the
* hung gdb, I get a similar stack trace, but with Python frames on the
* stack. The workaround given in the comments works here, too. I hesitate
* to point fingers, though, because I'm testing with a modern gdb-13.1
* compiled from source on a rather un-modern distro.
*/
Msg.warn(this, "gdb hung on quit. Sending SIGCONT.");
Runtime.getRuntime().exec("kill -SIGCONT %d".formatted(exec.gdb.pid()));
asyncQuit.get(QUIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
}
}
catch (TraceRmiError e) {
// expected
}
catch (ExecutionException e) {
if (!(e.getCause() instanceof TraceRmiError)) {
throw e;
}
}
GdbResult r = exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
r.handle();
waitForPass(() -> assertTrue(connection.isClosed()));
}
finally {
exec.gdb.destroyForcibly();
}
}
}
protected GdbAndConnection startAndConnectGdb(Function<String, String> scriptSupplier)
throws Exception {
TraceRmiAcceptor acceptor = traceRmi.acceptOne(null);
ExecInGdb exec = execInGdb(scriptSupplier.apply(sockToStringForGdb(acceptor.getAddress())));
acceptor.setTimeout(CONNECT_TIMEOUT_MS);
try {
TraceRmiConnection connection = acceptor.accept();
return new GdbAndConnection(exec, connection);
}
catch (SocketTimeoutException e) {
exec.gdb.destroyForcibly();
exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle();
throw e;
}
}
protected GdbAndConnection startAndConnectGdb() throws Exception {
return startAndConnectGdb(addr -> """
%s
ghidra trace connect %s
""".formatted(PREAMBLE, addr));
}
@SuppressWarnings("resource")
protected String runThrowError(Function<String, String> scriptSupplier)
throws Exception {
GdbAndConnection conn = startAndConnectGdb(scriptSupplier);
GdbResult r = conn.exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
String stdout = r.handle();
waitForPass(() -> assertTrue(conn.connection.isClosed()));
return stdout;
}
protected void waitState(int infnum, Supplier<Long> snapSupplier, TargetExecutionState state) {
TraceObjectKeyPath infPath = TraceObjectKeyPath.parse("Inferiors").index(infnum);
TraceObject inf =
Objects.requireNonNull(tb.trace.getObjectManager().getObjectByCanonicalPath(infPath));
waitForPass(
() -> assertEquals(state.name(), tb.objValue(inf, snapSupplier.get(), "_state")));
waitTxDone();
}
protected void waitStopped() {
waitState(1, () -> 0L, TargetExecutionState.STOPPED);
}
protected void waitRunning() {
waitState(1, () -> 0L, TargetExecutionState.RUNNING);
}
protected String extractOutSection(String out, String head) {
return out.split(head)[1].split("---")[0].replace("(gdb)", "").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+"));
assertThat(toksLine0.get(0), startsWith("0x"));
long address = Long.decode(toksLine0.get(0));
ByteArrayOutputStream buf = new ByteArrayOutputStream();
for (String l : lines) {
List<String> parts = List.of(l.split(":"));
assertEquals(2, parts.size());
String hex = parts.get(1).replaceAll("\\s*0x", "");
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, String key, Address addr, int len,
Set<TraceBreakpointKind> kinds, String expression) throws Exception {
assertEquals(key, locVal.getEntryKey());
TraceObject loc = locVal.getChild();
TraceObject spec = loc.getCanonicalParent(0).getParent();
assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue());
assertEquals(TraceBreakpointKindSet.encode(kinds), spec.getValue(0, "_kinds").getValue());
assertEquals(expression, spec.getValue(0, "_expression").getValue());
}
protected void waitTxDone() {
waitFor(() -> tb.trace.getCurrentTransactionInfo() == null);
}
private record Cut(String head, int begin, int end) {
String parseCell(String line) {
int begin = Math.min(line.length(), this.begin);
int end = Math.min(line.length(), this.end);
/**
* NOTE: Do not assert previous char is space.
*
* When breakpoints table spells out locations, Address and What cells are indented and
* no longer align with their column headers.
*/
return line.substring(begin, end).trim();
}
}
protected record Row(Map<String, String> cells) {
private static Row parse(List<Cut> cuts, String line) {
return new Row(
cuts.stream().collect(Collectors.toMap(Cut::head, c -> c.parseCell(line))));
}
public String getCell(String head) {
return cells.get(head);
}
}
protected record Tabular(List<String> headings, List<Row> rows) {
static final Pattern SPACES = Pattern.compile(" *");
static final Pattern WORDS = Pattern.compile("\\w+");
private static List<Cut> findCuts(String header) {
List<Cut> result = new ArrayList<>();
Matcher spaceMatcher = SPACES.matcher(header);
Matcher wordMatcher = WORDS.matcher(header);
int start = 0;
while (start < header.length()) {
if (!spaceMatcher.find(start)) {
throw new AssertionError();
}
start = spaceMatcher.end();
if (start >= header.length()) {
break;
}
if (!wordMatcher.find(start)) {
throw new AssertionError();
}
result.add(new Cut(wordMatcher.group(), wordMatcher.start(), wordMatcher.end()));
start = wordMatcher.end();
}
return result;
}
private static List<Cut> adjustCuts(List<Cut> cuts) {
List<Cut> result = new ArrayList<>();
for (int i = 0; i < cuts.size(); i++) {
Cut cut = cuts.get(i);
int j = i + 1;
int end = j < cuts.size() ? cuts.get(j).begin : Integer.MAX_VALUE;
result.add(new Cut(cut.head, cut.begin, end));
}
return result;
}
/**
* Parse a table.
*
* <p>
* This is far from perfect, but good enough for making assertions in tests. For example, in
* the breakpoints table, gdb may insert an extra informational line under a breakpoint row.
* This line will get mangled and parsed as if it were an entry. However, it's "Num" cell
* will be empty, so they will not likely interfere.
*
* @param out the output in tabular form
* @return the table object, more or less
*/
public static Tabular parse(String out) {
List<String> lines = List.of(out.split("\n"));
if (lines.isEmpty()) {
throw new AssertionError("Output is not tabular");
}
List<Cut> cuts = adjustCuts(findCuts(lines.get(0)));
return new Tabular(cuts.stream().map(Cut::head).toList(),
lines.stream().skip(1).map(l -> Row.parse(cuts, l)).toList());
}
public Row findRow(String head, String contents) {
return rows.stream()
.filter(r -> Objects.equals(contents, r.getCell(head)))
.findFirst()
.orElse(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

@ -1,428 +0,0 @@
/* ###
* 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.gdb.rmi;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import java.util.List;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import generic.test.category.NightlyCategory;
import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.testutil.DummyProc;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathPredicates;
import ghidra.program.model.address.AddressSpace;
import ghidra.trace.database.ToyDBTraceBuilder;
import ghidra.trace.model.Trace;
import ghidra.trace.model.memory.TraceMemorySpace;
import ghidra.trace.model.target.TraceObject;
import ghidra.trace.model.time.TraceSnapshot;
@Category(NightlyCategory.class) // this may actually be an @PortSensitive test
public class GdbHooksTest extends AbstractGdbTraceRmiTest {
private static final long RUN_TIMEOUT_MS = 20000;
private static final long RETRY_MS = 500;
record GdbAndTrace(GdbAndConnection 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 {
conn.close();
mdo.close();
}
}
@SuppressWarnings("resource")
protected GdbAndTrace startAndSyncGdb() throws Exception {
GdbAndConnection conn = startAndConnectGdb();
try {
// TODO: Why does using 'set arch' cause a hang at quit?
conn.execute("""
set ghidra-language x86:LE:64:default
ghidra trace start
ghidra trace sync-enable""");
ManagedDomainObject mdo = waitDomainObject("/New Traces/gdb/noname");
tb = new ToyDBTraceBuilder((Trace) mdo.get());
return new GdbAndTrace(conn, mdo);
}
catch (Exception e) {
conn.close();
throw e;
}
}
@Test
public void testOnNewInferior() throws Exception {
try (GdbAndTrace conn = startAndSyncGdb()) {
conn.execute("add-inferior");
waitForPass(() -> assertEquals(2, tb.objValues(0, "Inferiors[]").size()));
}
}
protected String getIndex(TraceObject object, String pattern) {
if (object == null) {
return null;
}
PathPattern pat = PathPredicates.parse(pattern).getSingletonPattern();
if (pat.countWildcards() != 1) {
throw new IllegalArgumentException("Exactly one wildcard required");
}
List<String> path = object.getCanonicalPath().getKeyList();
if (path.size() < pat.asPath().size()) {
return null;
}
List<String> matched = pat.matchKeys(path.subList(0, pat.asPath().size()));
if (matched == null) {
return null;
}
return matched.get(0);
}
protected String inferiorIndex(TraceObject object) {
return getIndex(object, "Inferiors[]");
}
@Test
public void testOnInferiorSelected() throws Exception {
try (GdbAndTrace conn = startAndSyncGdb()) {
traceManager.openTrace(tb.trace);
// Both inferiors must have sync enabled
conn.execute("""
add-inferior
inferior 2
ghidra trace sync-enable""");
conn.execute("inferior 1");
waitForPass(() -> assertEquals("1", inferiorIndex(traceManager.getCurrentObject())));
conn.execute("inferior 2");
waitForPass(() -> assertEquals("2", inferiorIndex(traceManager.getCurrentObject())));
conn.execute("inferior 1");
waitForPass(() -> assertEquals("1", inferiorIndex(traceManager.getCurrentObject())));
}
}
@Test
public void testOnInferiorDeleted() throws Exception {
try (GdbAndTrace conn = startAndSyncGdb()) {
conn.execute("add-inferior");
waitForPass(() -> assertEquals(2, tb.objValues(0, "Inferiors[]").size()));
conn.execute("remove-inferior 2");
waitForPass(() -> assertEquals(1, tb.objValues(0, "Inferiors[]").size()));
}
}
protected long lastSnap(GdbAndTrace conn) {
return conn.conn.connection().getLastSnapshot(tb.trace);
}
@Test
public void testOnNewThread() throws Exception {
String cloneExit = DummyProc.which("expCloneExit");
try (GdbAndTrace conn = startAndSyncGdb()) {
conn.execute("""
file %s
break work
start""".formatted(cloneExit));
waitForPass(() -> {
TraceObject inf = tb.obj("Inferiors[1]");
assertNotNull(inf);
assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
waitForPass(() -> assertEquals(1,
tb.objValues(lastSnap(conn), "Inferiors[1].Threads[]").size()),
RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("continue");
waitForPass(() -> assertEquals(2,
tb.objValues(lastSnap(conn), "Inferiors[1].Threads[]").size()),
RUN_TIMEOUT_MS, RETRY_MS);
}
}
protected String threadIndex(TraceObject object) {
return getIndex(object, "Inferiors[1].Threads[]");
}
@Test
public void testOnThreadSelected() throws Exception {
String cloneExit = DummyProc.which("expCloneExit");
try (GdbAndTrace conn = startAndSyncGdb()) {
traceManager.openTrace(tb.trace);
conn.execute("""
file %s
break work
run""".formatted(cloneExit));
waitForPass(() -> {
TraceObject inf = tb.obj("Inferiors[1]");
assertNotNull(inf);
assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
waitForPass(() -> assertEquals(2,
tb.objValues(lastSnap(conn), "Inferiors[1].Threads[]").size()),
RUN_TIMEOUT_MS, RETRY_MS);
// Now the real test
conn.execute("thread 1");
waitForPass(() -> assertEquals("1", threadIndex(traceManager.getCurrentObject())));
conn.execute("thread 2");
waitForPass(() -> assertEquals("2", threadIndex(traceManager.getCurrentObject())));
conn.execute("thread 1");
waitForPass(() -> assertEquals("1", threadIndex(traceManager.getCurrentObject())));
}
}
protected String frameIndex(TraceObject object) {
return getIndex(object, "Inferiors[1].Threads[1].Stack[]");
}
@Test
public void testOnFrameSelected() throws Exception {
String stack = DummyProc.which("expStack");
try (GdbAndTrace conn = startAndSyncGdb()) {
traceManager.openTrace(tb.trace);
conn.execute("""
file %s
break break_here
run""".formatted(stack));
waitForPass(() -> assertThat(
tb.objValues(lastSnap(conn), "Inferiors[1].Threads[1].Stack[]").size(),
greaterThan(2)),
RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("frame 1");
waitForPass(() -> assertEquals("1", frameIndex(traceManager.getCurrentObject())),
RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("frame 0");
waitForPass(() -> assertEquals("0", frameIndex(traceManager.getCurrentObject())),
RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
@Ignore
public void testOnSyscallMemory() throws Exception {
// TODO: Need a specimen
// FWIW, I've already seen this getting exercised in other tests.
}
@Test
public void testOnMemoryChanged() throws Exception {
try (GdbAndTrace conn = startAndSyncGdb()) {
conn.execute("""
file bash
start""");
long address = Long.decode(conn.executeCapture("print/x &main").split("\\s+")[2]);
conn.execute("set *((char*) &main) = 0x7f");
waitForPass(() -> {
ByteBuffer buf = ByteBuffer.allocate(1);
tb.trace.getMemoryManager().getBytes(lastSnap(conn), tb.addr(address), buf);
assertEquals(0x7f, buf.get(0));
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnRegisterChanged() throws Exception {
try (GdbAndTrace conn = startAndSyncGdb()) {
conn.execute("""
file bash
start""");
TraceObject thread = waitForValue(() -> tb.obj("Inferiors[1].Threads[1]"));
waitForPass(
() -> assertEquals("STOPPED", tb.objValue(thread, lastSnap(conn), "_state")),
RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("set $rax = 0x1234");
AddressSpace space = tb.trace.getBaseAddressFactory()
.getAddressSpace("Inferiors[1].Threads[1].Stack[0].Registers");
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 (GdbAndTrace conn = startAndSyncGdb()) {
conn.execute("""
file bash
run""");
TraceObject inf = waitForValue(() -> tb.obj("Inferiors[1]"));
TraceObject thread = waitForValue(() -> tb.obj("Inferiors[1].Threads[1]"));
waitForPass(() -> {
assertEquals("RUNNING", tb.objValue(inf, lastSnap(conn), "_state"));
assertEquals("RUNNING", tb.objValue(thread, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnStop() throws Exception {
try (GdbAndTrace conn = startAndSyncGdb()) {
conn.execute("""
file bash
start""");
TraceObject inf = waitForValue(() -> tb.obj("Inferiors[1]"));
TraceObject thread = waitForValue(() -> tb.obj("Inferiors[1].Threads[1]"));
waitForPass(() -> {
assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state"));
assertEquals("STOPPED", tb.objValue(thread, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnExited() throws Exception {
try (GdbAndTrace conn = startAndSyncGdb()) {
conn.execute("""
file bash
set args -c "exit 1"
run""");
waitForPass(() -> {
TraceSnapshot snapshot =
tb.trace.getTimeManager().getSnapshot(lastSnap(conn), false);
assertNotNull(snapshot);
assertEquals("Exited with code 1", snapshot.getDescription());
TraceObject inf1 = tb.obj("Inferiors[1]");
assertNotNull(inf1);
Object val = tb.objValue(inf1, lastSnap(conn), "_exit_code");
assertThat(val, instanceOf(Number.class));
assertEquals(1, ((Number) val).longValue());
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
/**
* Test on_clear_objfiles, on_new_objfile, on_free_objfile.
*
* <p>
* Technically, this probably doesn't hit on_free_objfile, but all three just call
* modules_changed, so I'm not concerned.
*/
@Test
public void testOnEventsObjfiles() throws Exception {
String print = DummyProc.which("expPrint");
String modPrint = "Inferiors[1].Modules[%s]".formatted(print);
try (GdbAndTrace conn = startAndSyncGdb()) {
conn.execute("""
file %s
start""".formatted(print));
waitForPass(() -> assertEquals(1, tb.objValues(lastSnap(conn), modPrint).size()),
RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("continue");
waitState(1, () -> lastSnap(conn), TargetExecutionState.TERMINATED);
/**
* Termination does not clear objfiles. Not until we run a new target.
*/
conn.execute("""
file bash
set args -c "exit 1"
run""");
waitForPass(() -> assertEquals(0, tb.objValues(lastSnap(conn), modPrint).size()),
RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnBreakpointCreated() throws Exception {
String print = DummyProc.which("expPrint");
try (GdbAndTrace conn = startAndSyncGdb()) {
conn.execute("file " + print);
assertEquals(0, tb.objValues(lastSnap(conn), "Breakpoints[]").size());
conn.execute("break main");
waitForPass(() -> {
List<Object> brks = tb.objValues(lastSnap(conn), "Breakpoints[]");
assertEquals(1, brks.size());
return (TraceObject) brks.get(0);
});
}
}
@Test
public void testOnBreakpointModified() throws Exception {
String print = DummyProc.which("expPrint");
try (GdbAndTrace conn = startAndSyncGdb()) {
conn.execute("file " + print);
assertEquals(0, tb.objValues(lastSnap(conn), "Breakpoints[]").size());
conn.execute("break main");
TraceObject brk = waitForPass(() -> {
List<Object> brks = tb.objValues(lastSnap(conn), "Breakpoints[]");
assertEquals(1, brks.size());
return (TraceObject) brks.get(0);
});
assertEquals(null, tb.objValue(brk, lastSnap(conn), "Commands"));
conn.execute("""
commands %s
echo test
end""".formatted(brk.getCanonicalPath().index()));
waitForPass(
() -> assertEquals("echo test\n", tb.objValue(brk, lastSnap(conn), "Commands")));
}
}
@Test
public void testOnBreakpointDeleted() throws Exception {
String print = DummyProc.which("expPrint");
try (GdbAndTrace conn = startAndSyncGdb()) {
conn.execute("file " + print);
assertEquals(0, tb.objValues(lastSnap(conn), "Breakpoints[]").size());
conn.execute("break main");
TraceObject brk = waitForPass(() -> {
List<Object> brks = tb.objValues(lastSnap(conn), "Breakpoints[]");
assertEquals(1, brks.size());
return (TraceObject) brks.get(0);
});
conn.execute("delete %s".formatted(brk.getCanonicalPath().index()));
waitForPass(
() -> assertEquals(0, tb.objValues(lastSnap(conn), "Breakpoints[]").size()));
}
}
}

View file

@ -1,517 +0,0 @@
/* ###
* 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.rmi;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.*;
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.junit.Before;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin;
import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
import ghidra.app.services.TraceRmiService;
import ghidra.dbg.testutil.DummyProc;
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.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.Msg;
import ghidra.util.NumericUtilities;
public abstract class AbstractLldbTraceRmiTest extends AbstractGhidraHeadedDebuggerGUITest {
/**
* 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 = """
script import ghidralldb
settings set target.disable-aslr false
""";
// 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;
public static final String INSTRUMENT_STOPPED =
"""
ghidra_trace_txopen "Fake" 'ghidra_trace_create_obj Processes[1]'
define do-set-stopped
ghidra_trace_set_value Processes[1] _state '"STOPPED"'
end
define set-stopped
ghidra_trace_txopen Stopped do-set-stopped
end
#lldb.debugger.HandleCommand('target stop-hook add -P ghidralldb.hooks.StopHook')
#python lldb.events.stop.connect(lambda e: lldb.execute("set-stopped"))""";
public static final String INSTRUMENT_RUNNING =
"""
ghidra_trace_txopen "Fake" 'ghidra_trace_create_obj Processes[1]'
define do-set-running
ghidra_trace_set_value Processes[1] _state '"RUNNING"'
end
define set-running
ghidra_trace_txopen Running do-set-running
end
#lldb.debugger.HandleCommand('target stop-hook add -P ghidralldb.hooks.StopHook')
#python lldb.events.cont.connect(lambda e: lldb.execute("set-running"))""";
protected TraceRmiService traceRmi;
private Path lldbPath;
private Path outFile;
private Path errFile;
// @BeforeClass
public static void setupPython() throws Throwable {
new ProcessBuilder("gradle", "Debugger-agent-lldb:assemblePyPackage")
.directory(TestApplicationUtils.getInstallationDirectory())
.inheritIO()
.start()
.waitFor();
}
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-lldb",
"build/pypkg/src").getAbsolutePath();
String add = rmiPyPkg + sep + gdbPyPkg;
pb.environment().compute("PYTHONPATH", (k, v) -> v == null ? add : (v + sep + add));
}
@Before
public void setupTraceRmi() throws Throwable {
traceRmi = addPlugin(tool, TraceRmiPlugin.class);
try {
lldbPath = Paths.get(DummyProc.which("lldb-16"));
}
catch (RuntimeException e) {
lldbPath = Paths.get(DummyProc.which("lldb"));
}
outFile = Files.createTempFile("lldbout", null);
errFile = Files.createTempFile("lldberr", 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 addrToStringForLldb(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 sockToStringForLldb(SocketAddress address) {
if (address instanceof InetSocketAddress tcp) {
return addrToStringForLldb(tcp.getAddress()) + ":" + tcp.getPort();
}
throw new AssertionError("Unhandled address type " + address);
}
protected record LldbResult(boolean timedOut, int exitCode, String stdout, String stderr) {
protected String handle() {
if (!"".equals(stderr) || (0 != exitCode && 143 != exitCode)) {
throw new LldbError(exitCode, stdout, stderr);
}
return stdout;
}
}
protected record ExecInLldb(Process lldb, CompletableFuture<LldbResult> future) {
}
@SuppressWarnings("resource") // Do not close stdin
protected ExecInLldb execInLldb(String script) throws IOException {
ProcessBuilder pb = new ProcessBuilder(lldbPath.toString());
setPythonPath(pb);
// If commands come from file, LLDB will quit after EOF.
Msg.info(this, "outFile: " + outFile);
Msg.info(this, "errFile: " + errFile);
pb.redirectInput(ProcessBuilder.Redirect.PIPE);
pb.redirectOutput(outFile.toFile());
pb.redirectError(errFile.toFile());
Process lldbProc = pb.start();
OutputStream stdin = lldbProc.getOutputStream();
stdin.write(script.getBytes());
stdin.flush();
return new ExecInLldb(lldbProc, CompletableFuture.supplyAsync(() -> {
try {
if (!lldbProc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
Msg.error(this, "Timed out waiting for LLDB");
lldbProc.destroyForcibly();
lldbProc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS);
return new LldbResult(true, -1, Files.readString(outFile),
Files.readString(errFile));
}
Msg.info(this, "LLDB exited with code " + lldbProc.exitValue());
return new LldbResult(false, lldbProc.exitValue(), Files.readString(outFile),
Files.readString(errFile));
}
catch (Exception e) {
return ExceptionUtils.rethrow(e);
}
finally {
lldbProc.destroyForcibly();
}
}));
}
public static class LldbError extends RuntimeException {
public final int exitCode;
public final String stdout;
public final String stderr;
public LldbError(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<LldbResult> result = execInLldb(script).future;
return result.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle();
}
protected record LldbAndConnection(ExecInLldb 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");
execute.invoke(Map.of("cmd", cmd));
}
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 lldb");
exec.lldb().destroy();
try {
LldbResult r = exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
r.handle();
waitForPass(() -> assertTrue(connection.isClosed()));
}
finally {
exec.lldb.destroyForcibly();
}
}
}
protected LldbAndConnection startAndConnectLldb(Function<String, String> scriptSupplier)
throws Exception {
TraceRmiAcceptor acceptor = traceRmi.acceptOne(null);
ExecInLldb exec =
execInLldb(scriptSupplier.apply(sockToStringForLldb(acceptor.getAddress())));
acceptor.setTimeout(CONNECT_TIMEOUT_MS);
try {
TraceRmiConnection connection = acceptor.accept();
return new LldbAndConnection(exec, connection);
}
catch (SocketTimeoutException e) {
exec.lldb.destroyForcibly();
exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle();
throw e;
}
}
protected LldbAndConnection startAndConnectLldb() throws Exception {
return startAndConnectLldb(addr -> """
%s
ghidra_trace_connect %s
""".formatted(PREAMBLE, addr));
}
@SuppressWarnings("resource")
protected String runThrowError(Function<String, String> scriptSupplier)
throws Exception {
LldbAndConnection conn = startAndConnectLldb(scriptSupplier);
LldbResult r = conn.exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
String stdout = r.handle();
waitForPass(() -> assertTrue(conn.connection.isClosed()));
return stdout;
}
protected void waitStopped() {
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]", Lifespan.at(0)));
waitForPass(() -> assertEquals("STOPPED", tb.objValue(proc, 0, "_state")));
waitTxDone();
}
protected void waitRunning() {
TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]", Lifespan.at(0)));
waitForPass(() -> assertEquals("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("(lldb)") && !s.equals("")) {
xout += s + "\n";
}
}
return xout.split(head)[1].split("---")[0].replace("(lldb)", "").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+"));
assertThat(toksLine0.get(0), startsWith("0x"));
String addrstr = toksLine0.get(0);
if (addrstr.contains(":")) {
addrstr = addrstr.substring(0, addrstr.indexOf(":"));
}
long address = Long.decode(addrstr);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
for (String l : lines) {
List<String> parts = List.of(l.split(":"));
assertEquals(2, parts.size());
String hex = parts.get(1).replaceAll("\\s*0x", "");
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, String key, Address addr, int len,
Set<TraceBreakpointKind> kinds, String expression) throws Exception {
assertEquals(key, locVal.getEntryKey());
TraceObject loc = locVal.getChild();
TraceObject spec = loc.getCanonicalParent(0).getParent();
assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue());
assertEquals(TraceBreakpointKindSet.encode(kinds), spec.getValue(0, "_kinds").getValue());
assertTrue(spec.getValue(0, "_expression").getValue().toString().contains(expression));
}
protected void assertWatchLoc(TraceObjectValue locVal, String key, Address addr, int len,
Set<TraceBreakpointKind> kinds, String expression) throws Exception {
assertEquals(key, locVal.getEntryKey());
TraceObject loc = locVal.getChild();
assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue());
assertEquals(TraceBreakpointKindSet.encode(kinds), loc.getValue(0, "_kinds").getValue());
}
protected void waitTxDone() {
waitFor(() -> tb.trace.getCurrentTransactionInfo() == null);
}
private record Cut(String head, int begin, int end) {
String parseCell(String line) {
int begin = Math.min(line.length(), this.begin);
int end = Math.min(line.length(), this.end);
/**
* NOTE: Do not assert previous char is space.
*
* When breakpoints table spells out locations, Address and What cells are indented and
* no longer align with their column headers.
*/
return line.substring(begin, end).trim();
}
}
protected record Row(Map<String, String> cells) {
private static Row parse(List<Cut> cuts, String line) {
return new Row(
cuts.stream().collect(Collectors.toMap(Cut::head, c -> c.parseCell(line))));
}
public String getCell(String head) {
return cells.get(head);
}
}
protected record Tabular(List<String> headings, List<Row> rows) {
static final Pattern SPACES = Pattern.compile(" *");
static final Pattern WORDS = Pattern.compile("\\w+");
private static List<Cut> findCuts(String header) {
List<Cut> result = new ArrayList<>();
Matcher spaceMatcher = SPACES.matcher(header);
Matcher wordMatcher = WORDS.matcher(header);
int start = 0;
while (start < header.length()) {
if (!spaceMatcher.find(start)) {
throw new AssertionError();
}
start = spaceMatcher.end();
if (start >= header.length()) {
break;
}
if (!wordMatcher.find(start)) {
throw new AssertionError();
}
result.add(new Cut(wordMatcher.group(), wordMatcher.start(), wordMatcher.end()));
start = wordMatcher.end();
}
return result;
}
private static List<Cut> adjustCuts(List<Cut> cuts) {
List<Cut> result = new ArrayList<>();
for (int i = 0; i < cuts.size(); i++) {
Cut cut = cuts.get(i);
int j = i + 1;
int end = j < cuts.size() ? cuts.get(j).begin : Integer.MAX_VALUE;
result.add(new Cut(cut.head, cut.begin, end));
}
return result;
}
/**
* Parse a table.
*
* <p>
* This is far from perfect, but good enough for making assertions in tests. For example, in
* the breakpoints table, lldb may insert an extra informational line under a breakpoint
* row. This line will get mangled and parsed as if it were an entry. However, it's "Num"
* cell will be empty, so they will not likely interfere.
*
* @param out the output in tabular form
* @return the table object, more or less
*/
public static Tabular parse(String out) {
List<String> lines = List.of(out.split("\n"));
if (lines.isEmpty()) {
throw new AssertionError("Output is not tabular");
}
List<Cut> cuts = adjustCuts(findCuts(lines.get(0)));
return new Tabular(cuts.stream().map(Cut::head).toList(),
lines.stream().skip(1).map(l -> Row.parse(cuts, l)).toList());
}
public Row findRow(String head, String contents) {
return rows.stream()
.filter(r -> Objects.equals(contents, r.getCell(head)))
.findFirst()
.orElse(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

@ -1,409 +0,0 @@
/* ###
* 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.rmi;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Objects;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import generic.test.category.NightlyCategory;
import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
import ghidra.dbg.testutil.DummyProc;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathPredicates;
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.time.TraceSnapshot;
@Category(NightlyCategory.class) // this may actually be an @PortSensitive test
public class LldbHooksTest extends AbstractLldbTraceRmiTest {
private static final long RUN_TIMEOUT_MS = 20000;
private static final long RETRY_MS = 500;
record LldbAndTrace(LldbAndConnection 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 {
conn.close();
mdo.close();
}
}
@SuppressWarnings("resource")
protected LldbAndTrace startAndSyncLldb() throws Exception {
LldbAndConnection conn = startAndConnectLldb();
try {
// TODO: Why does using 'set arch' cause a hang at quit?
conn.execute(
"ghidralldb.util.set_convenience_variable('ghidra-language', 'x86:LE:64:default')");
conn.execute("ghidra_trace_start");
ManagedDomainObject mdo = waitDomainObject("/New Traces/lldb/noname");
tb = new ToyDBTraceBuilder((Trace) mdo.get());
return new LldbAndTrace(conn, mdo);
}
catch (Exception e) {
conn.close();
throw e;
}
}
protected long lastSnap(LldbAndTrace conn) {
return conn.conn.connection().getLastSnapshot(tb.trace);
}
// TODO: This passes if you single-step through it but fails on some transactional stuff if run
//@Test
public void testOnNewThread() throws Exception {
String cloneExit = DummyProc.which("expCloneExit");
try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "%s".formatted(cloneExit));
conn.execute("break set -n work");
waitForPass(() -> {
TraceObject proc = tb.objAny("Processes[]");
assertNotNull(proc);
assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
txPut(conn, "threads");
waitForPass(() -> assertEquals(1,
tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()),
RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("continue");
waitStopped();
txPut(conn, "threads");
waitForPass(() -> assertEquals(2,
tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()),
RUN_TIMEOUT_MS, RETRY_MS);
}
}
// TODO: This passes if you single-step through it but fails on some transactional stuff if run
//@Test
public void testOnThreadSelected() throws Exception {
String cloneExit = DummyProc.which("expCloneExit");
try (LldbAndTrace conn = startAndSyncLldb()) {
traceManager.openTrace(tb.trace);
start(conn, "%s".formatted(cloneExit));
conn.execute("break set -n work");
waitForPass(() -> {
TraceObject inf = tb.objAny("Processes[]");
assertNotNull(inf);
assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
txPut(conn, "threads");
waitForPass(() -> assertEquals(1,
tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()),
RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("continue");
waitStopped();
waitForPass(() -> {
TraceObject inf = tb.objAny("Processes[]");
assertNotNull(inf);
assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
waitForPass(() -> assertEquals(2,
tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()),
RUN_TIMEOUT_MS, RETRY_MS);
// Now the real test
conn.execute("thread select 1");
conn.execute("frame select 0");
waitForPass(() -> {
String ti0 = conn.executeCapture("thread info");
assertTrue(ti0.contains("#1"));
String threadIndex = threadIndex(traceManager.getCurrentObject());
assertTrue(ti0.contains(threadIndex));
}, RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("thread select 2");
conn.execute("frame select 0");
waitForPass(() -> {
String ti0 = conn.executeCapture("thread info");
assertTrue(ti0.contains("#2"));
String threadIndex = threadIndex(traceManager.getCurrentObject());
assertTrue(ti0.contains(threadIndex));
}, RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("thread select 1");
conn.execute("frame select 0");
waitForPass(() -> {
String ti0 = conn.executeCapture("thread info");
assertTrue(ti0.contains("#1"));
String threadIndex = threadIndex(traceManager.getCurrentObject());
assertTrue(ti0.contains(threadIndex));
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
protected String getIndex(TraceObject object, String pattern, int n) {
if (object == null) {
return null;
}
PathPattern pat = PathPredicates.parse(pattern).getSingletonPattern();
// if (pat.countWildcards() != 1) {
// throw new IllegalArgumentException("Exactly one wildcard required");
// }
List<String> path = object.getCanonicalPath().getKeyList();
if (path.size() < pat.asPath().size()) {
return null;
}
List<String> matched = pat.matchKeys(path.subList(0, pat.asPath().size()));
if (matched == null) {
return null;
}
if (matched.size() <= n) {
return null;
}
return matched.get(n);
}
protected String threadIndex(TraceObject object) {
return getIndex(object, "Processes[].Threads[]", 1);
}
protected String frameIndex(TraceObject object) {
return getIndex(object, "Processes[].Threads[].Stack[]", 2);
}
@Test
public void testOnFrameSelected() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) {
traceManager.openTrace(tb.trace);
start(conn, "bash");
conn.execute("breakpoint set -n read");
conn.execute("cont");
waitStopped();
waitForPass(() -> assertThat(
tb.objValues(lastSnap(conn), "Processes[].Threads[].Stack[]").size(),
greaterThan(2)),
RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("frame select 1");
waitForPass(() -> assertEquals("1", frameIndex(traceManager.getCurrentObject())),
RUN_TIMEOUT_MS, RETRY_MS);
conn.execute("frame select 0");
waitForPass(() -> assertEquals("0", frameIndex(traceManager.getCurrentObject())),
RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
@Ignore
public void testOnSyscallMemory() throws Exception {
// TODO: Need a specimen
// FWIW, I've already seen this getting exercised in other tests.
}
@Test
public void testOnMemoryChanged() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash");
long address = Long.decode(conn.executeCapture("dis -c1 -n main").split("\\s+")[1]);
conn.execute("expr *((char*)(void(*)())main) = 0x7f");
conn.execute("ghidra_trace_txstart 'Tx'");
conn.execute("ghidra_trace_putmem `(void(*)())main` 10");
conn.execute("ghidra_trace_txcommit");
waitForPass(() -> {
ByteBuffer buf = ByteBuffer.allocate(10);
tb.trace.getMemoryManager().getBytes(lastSnap(conn), tb.addr(address), buf);
assertEquals(0x7f, buf.get(0));
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnRegisterChanged() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash");
conn.execute("expr $rax = 0x1234");
conn.execute("ghidra_trace_txstart 'Tx'");
conn.execute("ghidra_trace_putreg");
conn.execute("ghidra_trace_txcommit");
String path = "Processes[].Threads[].Stack[].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 (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash");
conn.execute("cont");
waitRunning();
TraceObject proc = waitForValue(() -> tb.objAny("Processes[]"));
waitForPass(() -> {
assertEquals("RUNNING", tb.objValue(proc, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnStop() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash");
TraceObject inf = waitForValue(() -> tb.objAny("Processes[]"));
waitForPass(() -> {
assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state"));
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnExited() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) {
conn.execute("file bash");
conn.execute("ghidra_trace_sync_enable");
conn.execute("process launch --stop-at-entry -- -c 'exit 1'");
txPut(conn, "processes");
conn.execute("cont");
waitRunning();
waitForPass(() -> {
TraceSnapshot snapshot =
tb.trace.getTimeManager().getSnapshot(lastSnap(conn), false);
assertNotNull(snapshot);
assertEquals("Exited with code 1", snapshot.getDescription());
TraceObject proc = tb.objAny("Processes[]");
assertNotNull(proc);
Object val = tb.objValue(proc, lastSnap(conn), "_exit_code");
assertThat(val, instanceOf(Number.class));
assertEquals(1, ((Number) val).longValue());
}, RUN_TIMEOUT_MS, RETRY_MS);
}
}
@Test
public void testOnBreakpointCreated() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash");
assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size());
conn.execute("breakpoint set -n main");
conn.execute("stepi");
waitForPass(() -> {
List<Object> brks = tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]");
assertEquals(1, brks.size());
return (TraceObject) brks.get(0);
});
}
}
@Test
public void testOnBreakpointModified() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash");
assertEquals(0, tb.objValues(lastSnap(conn), "Breakpoints[]").size());
conn.execute("breakpoint set -n main");
conn.execute("stepi");
TraceObject brk = waitForPass(() -> {
List<Object> brks = tb.objValues(lastSnap(conn), "Breakpoints[]");
assertEquals(1, brks.size());
return (TraceObject) brks.get(0);
});
assertEquals(null, tb.objValue(brk, lastSnap(conn), "Condition"));
conn.execute("breakpoint modify -c 'x>3'");
conn.execute("stepi");
// NB: Testing "Commands" requires multi-line input - not clear how to do this
//assertEquals(null, tb.objValue(brk, lastSnap(conn), "Commands"));
//conn.execute("breakpoint command add 'echo test'");
//conn.execute("DONE");
waitForPass(
() -> assertEquals("x>3", tb.objValue(brk, lastSnap(conn), "Condition")));
}
}
@Test
public void testOnBreakpointDeleted() throws Exception {
try (LldbAndTrace conn = startAndSyncLldb()) {
start(conn, "bash");
assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size());
conn.execute("breakpoint set -n main");
conn.execute("stepi");
TraceObject brk = waitForPass(() -> {
List<Object> brks = tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]");
assertEquals(1, brks.size());
return (TraceObject) brks.get(0);
});
conn.execute("breakpoint delete %s".formatted(brk.getCanonicalPath().index()));
conn.execute("stepi");
waitForPass(
() -> assertEquals(0,
tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size()));
}
}
private void start(LldbAndTrace conn, String obj) {
conn.execute("file " + obj);
conn.execute("ghidra_trace_sync_enable");
conn.execute("process launch --stop-at-entry");
txPut(conn, "processes");
}
private void txPut(LldbAndTrace conn, String obj) {
conn.execute("ghidra_trace_txstart 'Tx" + obj + "'");
conn.execute("ghidra_trace_put_" + obj);
conn.execute("ghidra_trace_txcommit");
}
}

View file

@ -24,13 +24,13 @@ import org.junit.Before;
import org.junit.Test;
import db.Transaction;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.services.TraceRmiLauncherService;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*;
import ghidra.util.task.ConsoleTaskMonitor;
public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebuggerGUITest {
public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebuggerTest {
TraceRmiLauncherService launcherService;
@Before
@ -43,11 +43,6 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug
return offers.stream().filter(o -> o.getTitle().equals(title)).findFirst().get();
}
@Test
public void testGetOpinions() throws Exception {
assertFalse(launcherService.getOpinions().isEmpty());
}
@Test
public void testGetOffers() throws Exception {
createProgram(getSLEIGH_X86_64_LANGUAGE());

View file

@ -31,13 +31,13 @@ import org.junit.Test;
import docking.widgets.OkDialog;
import docking.widgets.fieldpanel.support.*;
import ghidra.app.plugin.core.clipboard.ClipboardPlugin;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.services.*;
import ghidra.framework.OperatingSystem;
import ghidra.pty.*;
import ghidra.util.SystemUtilities;
public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest {
protected static byte[] ascii(String str) {
try {
return str.getBytes("UTF-8");