mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 10:49:34 +02:00
GP-3857: Port most Debugger components to TraceRmi.
This commit is contained in:
parent
7e4d2bcfaa
commit
fd4380c07a
222 changed files with 7241 additions and 3752 deletions
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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]);
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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());
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue