diff --git a/Ghidra/Debug/Debugger/ghidra_scripts/DemoDebuggerScript.java b/Ghidra/Debug/Debugger/ghidra_scripts/DemoDebuggerScript.java new file mode 100644 index 0000000000..ec4769a3ff --- /dev/null +++ b/Ghidra/Debug/Debugger/ghidra_scripts/DemoDebuggerScript.java @@ -0,0 +1,130 @@ +/* ### + * 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. + */ +//An example debugger script +//It launches the current program, places saved breakpoints, and runs it until termination. +//This script must be run from the Debugger tool, or another tool with the required plugins. +//This script has only been tested with /usr/bin/echo. +//@category Debugger +//@keybinding +//@menupath +//@toolbar + +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer.LaunchResult; +import ghidra.app.script.GhidraScript; +import ghidra.app.services.LogicalBreakpoint; +import ghidra.debug.flatapi.FlatDebuggerAPI; +import ghidra.program.model.address.Address; +import ghidra.trace.model.Trace; + +public class DemoDebuggerScript extends GhidraScript implements FlatDebuggerAPI { + + @Override + protected void run() throws Exception { + /** + * Here we'll just launch the current program. Note that this is not guaranteed to succeed + * at all. Launching is subject to an opinion-based service. If no offers are made, this + * will fail. If the target system is missing required components, this will fail. If the + * target behaves in an unexpected way, this may fail. One example is targets without an + * initial break. If Ghidra does not recognize the target platform, this will fail. Etc., + * etc., this may fail. + * + * In the event of failure, nothing is cleaned up automatically, since in some cases, the + * user may be expected to intervene. In our case; however, there's no way to continue this + * script on a repaired target, so we'll close the connection on failure. An alternative + * design for this script would expect the user to have already launched a target, and it + * would just operate on the "current target." + */ + println("Launching " + currentProgram); + LaunchResult result = launch(monitor); + if (result.exception() != null) { + printerr("Failed to launch " + currentProgram + ": " + result.exception()); + + if (result.model() != null) { + result.model().close(); + } + + if (result.recorder() != null) { + closeTrace(result.recorder().getTrace()); + } + return; + } + Trace trace = result.recorder().getTrace(); + println("Successfully launched in trace " + trace); + + /** + * Breakpoints are highly dependent on the module map. To work correctly: 1) The target + * debugger must provide the module map. 2) Ghidra must have recorded that module map into + * the trace. 3) Ghidra must recognize the module names and map them to programs open in the + * tool. These events all occur asynchronously, usually immediately after launch. Most + * launchers will wait for the target program module to be mapped to its Ghidra program + * database, but the breakpoint service may still be processing the new mapping. + */ + flushAsyncPipelines(trace); + + /** + * There is also breakpointsEnable(), but that operates on an address-by-address basis, + * which doesn't quite make sense in this case. We'll instead use getBreakpoints(Program) + * and enable them only in the new trace. The nested for is to deal with the fact that + * getBreakpoints(Program) returns a map from address to breakpoint set, i.e., a collection + * of collections. + */ + println("Enabling breakpoints"); + for (Set bs : getBreakpoints(currentProgram).values()) { + for (LogicalBreakpoint lb : bs) { + println(" " + lb); + if (lb.getTraceAddress(trace) == null) { + printerr(" Not mapped!"); + } + else { + waitOn(lb.enableForTrace(trace)); + } + } + } + + /** + * This runs the target, recording memory around the PC and SP at each break, until it + * terminates. + */ + while (isTargetAlive()) { + waitForBreak(10, TimeUnit.SECONDS); + /** + * The recorder is going to schedule some reads upon break, so let's allow them to + * settle. + */ + flushAsyncPipelines(trace); + + println("Reading PC"); + Address pc = getProgramCounter(); + println("Reading 1024 bytes at PC=" + pc); + readMemory(pc, 1024, monitor); + println("Reading SP"); + Address sp = getStackPointer(); + println("Reading 8096 bytes at SP=" + sp); + readMemory(sp, 8096, monitor); + /** + * Allow the commands we just issued to settle. + */ + flushAsyncPipelines(trace); + + println("Resuming"); + resume(); + } + println("Terminated"); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java index 4e9eccadd2..7e82157b0c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java @@ -261,6 +261,10 @@ public class DebuggerCoordinates { return all(trace, recorder, thread, view, newTime, frame); } + public DebuggerCoordinates withFrame(int newFrame) { + return all(trace, recorder, thread, view, time, newFrame); + } + public DebuggerCoordinates withView(TraceProgramView newView) { return all(trace, recorder, thread, newView, time, frame); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerStaticSyncTrait.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerStaticSyncTrait.java index 4d0579c54b..a68928d66d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerStaticSyncTrait.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerStaticSyncTrait.java @@ -196,7 +196,8 @@ public class DebuggerStaticSyncTrait { } protected void doSyncCursorIntoStatic(ProgramLocation location) { - if (location == null) { + DebuggerStaticMappingService mappingService = this.mappingService; + if (location == null || mappingService == null) { return; } ProgramLocation staticLoc = mappingService.getStaticLocationFromDynamic(location); @@ -207,8 +208,10 @@ public class DebuggerStaticSyncTrait { } protected void doSyncCursorFromStatic() { + ProgramLocation currentStaticLocation = this.currentStaticLocation; TraceProgramView view = current.getView(); // NB. Used for snap (don't want emuSnap) - if (view == null || currentStaticLocation == null) { + DebuggerStaticMappingService mappingService = this.mappingService; + if (currentStaticLocation == null || view == null || mappingService == null) { return; } ProgramLocation dynamicLoc = diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPlugin.java index f706b909d9..eeaff15f38 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPlugin.java @@ -18,6 +18,7 @@ package ghidra.app.plugin.core.debug.gui.breakpoint; import java.awt.Color; import java.awt.event.KeyEvent; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import javax.swing.SwingUtilities; @@ -861,7 +862,6 @@ public class DebuggerBreakpointMarkerPlugin extends Plugin } protected void doToggleBreakpointsAt(String title, ActionContext context) { - // TODO: Seems like this should be in logical breakpoint service? if (breakpointService == null) { return; } @@ -869,39 +869,21 @@ public class DebuggerBreakpointMarkerPlugin extends Plugin if (loc == null) { return; } - Set bs = breakpointService.getBreakpointsAt(loc); - if (bs == null || bs.isEmpty()) { + breakpointService.toggleBreakpointsAt(loc, () -> { Set supported = getSupportedKindsFromContext(context); if (supported.isEmpty()) { breakpointError(title, "It seems this target does not support breakpoints."); - return; + return CompletableFuture.completedFuture(Set.of()); } Set kinds = computeDefaultKinds(context, supported); long length = computeDefaultLength(context, kinds); placeBreakpointDialog.prompt(tool, breakpointService, title, loc, length, kinds, ""); - return; - } - State state = breakpointService.computeState(bs, loc); - /** - * If we're in the static listing, this will return null, indicating we should use the - * program's perspective. The methods taking trace should accept a null trace and behave - * accordingly. If in the dynamic listing, we act in the context of the returned trace. - */ - Trace trace = getTraceFromContext(context); - boolean mapped = breakpointService.anyMapped(bs, trace); - State toggled = state.getToggled(mapped); - if (toggled.isEnabled()) { - breakpointService.enableAll(bs, trace).exceptionally(ex -> { - breakpointError(title, "Could not enable breakpoints", ex); - return null; - }); - } - else { - breakpointService.disableAll(bs, trace).exceptionally(ex -> { - breakpointError(title, "Could not disable breakpoints", ex); - return null; - }); - } + // Not great, but I'm not sticking around for the dialog + return CompletableFuture.completedFuture(Set.of()); + }).exceptionally(ex -> { + breakpointError(title, "Could not toggle breakpoints", ex); + return null; + }); } /** diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java index ee652cabd3..30731308cc 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java @@ -1492,7 +1492,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter public void performLaunch(ActionContext context) { performAction(context, true, TargetLauncher.class, launcher -> { - Map args = launchOffer.getLauncherArgs(launcher.getParameters(), true); + Map args = launchOffer.getLauncherArgs(launcher, true); if (args == null) { // Cancelled return AsyncUtils.NIL; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java index c483a91a4c..93786b7ec4 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java @@ -18,8 +18,7 @@ package ghidra.app.plugin.core.debug.service.breakpoint; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; -import java.util.function.BiConsumer; -import java.util.function.Consumer; +import java.util.function.*; import java.util.stream.Collectors; import org.apache.commons.collections4.IteratorUtils; @@ -785,7 +784,7 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin protected void processChange(Consumer processor, String description) { executor.submit(() -> { - // Issue change callbacks without the lock! (try must surround sync) + // Invoke change callbacks without the lock! (try must surround sync) try (ChangeCollector c = new ChangeCollector(changeListeners.fire)) { synchronized (lock) { processor.accept(c); @@ -1206,6 +1205,29 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin }); } + @Override + public CompletableFuture> toggleBreakpointsAt(ProgramLocation loc, + Supplier>> placer) { + Set bs = getBreakpointsAt(loc); + if (bs == null || bs.isEmpty()) { + return placer.get(); + } + State state = computeState(bs, loc); + /** + * If we're in the static listing, this will return null, indicating we should use the + * program's perspective. The methods taking trace should accept a null trace and behave + * accordingly. If in the dynamic listing, we act in the context of the returned trace. + */ + Trace trace = + DebuggerLogicalBreakpointService.programOrTrace(loc, (p, a) -> null, (t, a) -> t); + boolean mapped = anyMapped(bs, trace); + State toggled = state.getToggled(mapped); + if (toggled.isEnabled()) { + return enableAll(bs, trace).thenApply(__ -> bs); + } + return disableAll(bs, trace).thenApply(__ -> bs); + } + @Override public void processEvent(PluginEvent event) { if (event instanceof ProgramOpenedPluginEvent) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java index e9a72e6620..8bf3dd7256 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java @@ -87,7 +87,8 @@ public class DebuggerModelServiceProxyPlugin extends Plugin private static final DebuggerProgramLaunchOffer DUMMY_LAUNCH_OFFER = new DebuggerProgramLaunchOffer() { @Override - public CompletableFuture launchProgram(TaskMonitor monitor, boolean prompt) { + public CompletableFuture launchProgram(TaskMonitor monitor, + boolean prompt, LaunchConfigurator configurator) { throw new AssertionError("Who clicked me?"); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorder.java index d17c12e916..ce47761f75 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorder.java @@ -562,9 +562,13 @@ public class DefaultTraceRecorder implements TraceRecorder { return true; } - // UNUSED? @Override public CompletableFuture flushTransactions() { - return parTx.flush(); + return CompletableFuture.runAsync(() -> { + }, privateQueue).thenCompose(__ -> { + return objectManager.flushEvents(); + }).thenCompose(__ -> { + return parTx.flush(); + }); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceEventListener.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceEventListener.java index 3f7fbf3e3f..eed3417735 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceEventListener.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceEventListener.java @@ -285,4 +285,8 @@ public class TraceEventListener extends AnnotatedDebuggerAttributeListener { reorderer.dispose(); } + public CompletableFuture flushEvents() { + return reorderer.flushEvents(); + } + } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectListener.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectListener.java index 93c7ddb2c0..710923d80f 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectListener.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectListener.java @@ -220,6 +220,10 @@ public class TraceObjectListener implements DebuggerModelListener { reorderer.dispose(); } + public CompletableFuture flushEvents() { + return reorderer.flushEvents(); + } + /* private CompletableFuture> findDependenciesTop(TargetObject added) { List result = new ArrayList<>(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectManager.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectManager.java index c535414056..f4b6cfca96 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectManager.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectManager.java @@ -702,4 +702,10 @@ public class TraceObjectManager { objectListener.dispose(); } + public CompletableFuture flushEvents() { + return eventListener.flushEvents().thenCompose(__ -> { + return objectListener.flushEvents(); + }); + } + } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java index d5fef0742e..7f78c98fac 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java @@ -23,7 +23,6 @@ import java.util.stream.Collectors; import javax.swing.JOptionPane; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.jdom.Element; import org.jdom.JDOMException; @@ -35,6 +34,7 @@ import ghidra.dbg.*; import ghidra.dbg.target.*; import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; import ghidra.dbg.target.TargetMethod.ParameterDescription; +import ghidra.dbg.target.TargetMethod.TargetParameterMap; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.util.PathUtils; import ghidra.framework.model.DomainFile; @@ -276,11 +276,14 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg * @param params the parameters of the model's launcher * @return the arguments given by the user, or null if cancelled */ - protected Map promptLauncherArgs(Map> params) { + protected Map promptLauncherArgs(TargetLauncher launcher, + LaunchConfigurator configurator) { + TargetParameterMap params = launcher.getParameters(); DebuggerMethodInvocationDialog dialog = new DebuggerMethodInvocationDialog(tool, getButtonTitle(), "Launch", getIcon()); // NB. Do not invoke read/writeConfigState - Map args = loadLastLauncherArgs(params, true); + Map args = configurator.configureLauncher(launcher, + loadLastLauncherArgs(launcher, true), RelPrompt.BEFORE); for (ParameterDescription param : params.values()) { Object val = args.get(param.name); if (val != null) { @@ -311,13 +314,13 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg * @param forPrompt true if the user will be confirming the arguments * @return the loaded arguments, or defaults */ - protected Map loadLastLauncherArgs( - Map> params, boolean forPrompt) { + protected Map loadLastLauncherArgs(TargetLauncher launcher, boolean forPrompt) { /** * TODO: Supposedly, per-program, per-user config stuff is being generalized for analyzers. * Re-examine this if/when that gets merged */ if (program != null) { + TargetParameterMap params = launcher.getParameters(); ProgramUserData userData = program.getProgramUserData(); String property = userData.getStringProperty(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, null); @@ -354,7 +357,6 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg } return new LinkedHashMap<>(); - } /** @@ -368,11 +370,17 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg * @param params the parameters of the model's launcher * @return the chosen arguments, or null if the user cancels at the prompt */ - public Map getLauncherArgs(Map> params, - boolean prompt) { + public Map getLauncherArgs(TargetLauncher launcher, + boolean prompt, LaunchConfigurator configurator) { return prompt - ? promptLauncherArgs(params) - : loadLastLauncherArgs(params, false); + ? configurator.configureLauncher(launcher, + promptLauncherArgs(launcher, configurator), RelPrompt.AFTER) + : configurator.configureLauncher(launcher, loadLastLauncherArgs(launcher, false), + RelPrompt.NONE); + } + + public Map getLauncherArgs(TargetLauncher launcher, boolean prompt) { + return getLauncherArgs(launcher, prompt, LaunchConfigurator.NOP); } /** @@ -431,8 +439,9 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg } protected CompletableFuture connect(DebuggerModelService service, - boolean prompt) { + boolean prompt, LaunchConfigurator configurator) { DebuggerModelFactory factory = getModelFactory(); + configurator.configureConnector(factory); if (prompt) { return service.showConnectDialog(factory); } @@ -454,8 +463,8 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg // Eww. protected CompletableFuture launch(TargetLauncher launcher, - boolean prompt) { - Map args = getLauncherArgs(launcher.getParameters(), prompt); + boolean prompt, LaunchConfigurator configurator) { + Map args = getLauncherArgs(launcher, prompt, configurator); if (args == null) { throw new CancellationException(); } @@ -551,17 +560,27 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg } @Override - public CompletableFuture launchProgram(TaskMonitor monitor, boolean prompt) { + public CompletableFuture launchProgram(TaskMonitor monitor, boolean prompt, + LaunchConfigurator configurator) { DebuggerModelService service = tool.getService(DebuggerModelService.class); DebuggerStaticMappingService mappingService = tool.getService(DebuggerStaticMappingService.class); monitor.initialize(6); monitor.setMessage("Connecting"); var locals = new Object() { + DebuggerObjectModel model; CompletableFuture futureTarget; + TargetObject target; + TraceRecorder recorder; + Throwable exception; + + LaunchResult getResult() { + return new LaunchResult(model, target, recorder, exception); + } }; - return connect(service, prompt).thenCompose(m -> { + return connect(service, prompt, configurator).thenCompose(m -> { checkCancelled(monitor); + locals.model = m; monitor.incrementProgress(1); monitor.setMessage("Finding Launcher"); return AsyncTimer.DEFAULT_TIMER.mark() @@ -573,7 +592,7 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg monitor.setMessage("Launching"); locals.futureTarget = listenForTarget(l.getModel()); return AsyncTimer.DEFAULT_TIMER.mark() - .timeOut(launch(l, prompt), getTimeoutMillis(), + .timeOut(launch(l, prompt, configurator), getTimeoutMillis(), () -> onTimedOutLaunch(monitor)); }).thenCompose(__ -> { checkCancelled(monitor); @@ -584,6 +603,7 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg () -> onTimedOutTarget(monitor)); }).thenCompose(t -> { checkCancelled(monitor); + locals.target = t; monitor.incrementProgress(1); monitor.setMessage("Waiting for recorder"); return AsyncTimer.DEFAULT_TIMER.mark() @@ -591,6 +611,7 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg () -> onTimedOutRecorder(monitor, service, t)); }).thenCompose(r -> { checkCancelled(monitor); + locals.recorder = r; monitor.incrementProgress(1); if (r == null) { throw new CancellationException(); @@ -600,10 +621,16 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg .timeOut(listenForMapping(mappingService, r), getTimeoutMillis(), () -> onTimedOutMapping(monitor, mappingService, r)); }).exceptionally(ex -> { - if (AsyncUtils.unwrapThrowable(ex) instanceof CancellationException) { - return null; + locals.exception = AsyncUtils.unwrapThrowable(ex); + return null; + }).thenApply(__ -> { + if (locals.exception != null) { + monitor.setMessage("Launch error: " + locals.exception); + return locals.getResult(); } - return ExceptionUtils.rethrow(ex); + monitor.setMessage("Launch successful"); + monitor.incrementProgress(1); + return locals.getResult(); }); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOffer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOffer.java index 7df6b583fc..e979139796 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOffer.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOffer.java @@ -15,11 +15,17 @@ */ package ghidra.app.plugin.core.debug.service.model.launch; +import java.util.Map; import java.util.concurrent.CompletableFuture; import javax.swing.Icon; import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.services.TraceRecorder; +import ghidra.dbg.DebuggerModelFactory; +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.target.TargetLauncher; +import ghidra.dbg.target.TargetObject; import ghidra.util.task.TaskMonitor; /** @@ -32,6 +38,89 @@ import ghidra.util.task.TaskMonitor; */ public interface DebuggerProgramLaunchOffer { + /** + * The result of launching a program + * + *

+ * The launch may not always be completely successful. Instead of tearing things down, partial + * launches are left in place, in case the user wishes to repair/complete the steps manually. If + * the result includes a recorder, the launch was completed successfully. If not, then the + * caller can choose how to treat the connection and target. If the cause of failure was an + * exception, it is included. If the launch succeeded, but module mapping failed, the result + * will include a recorder and the exception. + * + * @param model the connection + * @param target the launched target + * @param recorder the recorder + * @param exception optional error, if failed + */ + public record LaunchResult(DebuggerObjectModel model, TargetObject target, + TraceRecorder recorder, Throwable exception) { + public static LaunchResult totalFailure(Throwable ex) { + return new LaunchResult(null, null, null, ex); + } + } + + /** + * When programmatically customizing launch configuration, describes callback timing relative to + * prompting the user. + */ + public enum RelPrompt { + /** + * The user is not prompted for parameters. This will be the only callback. + */ + NONE, + /** + * The user will be prompted. This callback can pre-populate suggested parameters. Another + * callback will be issued if the user does not cancel. + */ + BEFORE, + /** + * The user has confirmed the parameters. This callback can validate or override the users + * parameters. Overriding the user is discouraged. This is the final callback. + */ + AFTER; + } + + /** + * Callbacks for custom configuration when launching a program + */ + public interface LaunchConfigurator { + LaunchConfigurator NOP = new LaunchConfigurator() {}; + + /** + * Re-configure the factory, if desired + * + * @param factory the factory that will create the connection + */ + default void configureConnector(DebuggerModelFactory factory) { + } + + /** + * Re-write the launcher arguments, if desired + * + * @param launcher the launcher that will create the target + * @param arguments the arguments suggested by the offer or saved settings + * @param relPrompt describes the timing of this callback relative to prompting the user + * @return the adjusted arguments + */ + default Map configureLauncher(TargetLauncher launcher, + Map arguments, RelPrompt relPrompt) { + return arguments; + } + } + + /** + * Launch the program using the offered mechanism + * + * @param monitor a monitor for progress and cancellation + * @param prompt if the user should be prompted to confirm launch parameters + * @param configurator the configuration callbacks + * @return a future which completes when the program is launched + */ + CompletableFuture launchProgram(TaskMonitor monitor, boolean prompt, + LaunchConfigurator configurator); + /** * Launch the program using the offered mechanism * @@ -39,7 +128,9 @@ public interface DebuggerProgramLaunchOffer { * @param prompt if the user should be prompted to confirm launch parameters * @return a future which completes when the program is launched */ - CompletableFuture launchProgram(TaskMonitor monitor, boolean prompt); + default CompletableFuture launchProgram(TaskMonitor monitor, boolean prompt) { + return launchProgram(monitor, prompt, LaunchConfigurator.NOP); + } /** * A name so that this offer can be recognized later diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java index 135fb85288..64f8979933 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java @@ -599,7 +599,7 @@ public class DebuggerStaticMappingServicePlugin extends Plugin @Override public CompletableFuture changesSettled() { - return changeDebouncer.settled(); + return changeDebouncer.stable(); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerLogicalBreakpointService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerLogicalBreakpointService.java index 054b9a7c12..60994fb840 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerLogicalBreakpointService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerLogicalBreakpointService.java @@ -18,6 +18,7 @@ package ghidra.app.services; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; +import java.util.function.Supplier; import ghidra.app.plugin.core.debug.service.breakpoint.DebuggerLogicalBreakpointServicePlugin; import ghidra.app.services.LogicalBreakpoint.State; @@ -351,4 +352,14 @@ public interface DebuggerLogicalBreakpointService { * @return a future which completes when the command has been processed */ CompletableFuture deleteLocs(Collection col); + + /** + * Toggle the breakpoints at the given location + * + * @param location the location + * @param placer if there are no breakpoints, a routine for placing a breakpoint + * @return a future which completes when the command has been processed + */ + CompletableFuture> toggleBreakpointsAt(ProgramLocation location, + Supplier>> placer); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/LogicalBreakpoint.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/LogicalBreakpoint.java index 4a506dd361..5528ad0389 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/LogicalBreakpoint.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/LogicalBreakpoint.java @@ -742,12 +742,13 @@ public interface LogicalBreakpoint { * Enable (or create) this breakpoint in the given target. * *

- * Presuming the breakpoint is mappable to the given trace, if no breakpoint of the same kind - * exists at the mapped address, then this will create a new breakpoint. Note, depending on the - * debugging model, the enabled or created breakpoint may apply to more than the given trace. + * If the breakpoint already exists, it is enabled. If it's already enabled, this has no effect. + * If not, and the breakpoint is mappable to the given trace, the breakpoint is created. Note, + * depending on the debugging model, the enabled or created breakpoint may affect other targets. + * If the breakpoint is not mappable to the given trace, this has no effect. * *

- * This simply issues the command. The logical breakpoint is updated only when the resulting + * This simply issues the command(s). The logical breakpoint is updated only when the resulting * events are processed. * * @param trace the trace for the given target @@ -761,7 +762,7 @@ public interface LogicalBreakpoint { *

* Note this will not create any new breakpoints. It will disable all breakpoints of the same * kind at the mapped address. Note, depending on the debugging model, the disabled breakpoint - * may apply to more than the given trace. + * may affect other targets. * *

* This simply issues the command. The logical breakpoint is updated only when the resulting @@ -779,7 +780,7 @@ public interface LogicalBreakpoint { * This presumes the breakpoint's specifications are deletable. Note that if the logical * breakpoint is still mappable into this trace, a marker may be displayed, even though no * breakpoint is actually present. Note, depending on the debugging model, the deleted - * breakpoint may be removed from more than the given trace. + * breakpoint may be removed from other targets. * * This simply issues the command. The logical breakpoint is updated only when the resulting * events are processed. @@ -794,7 +795,7 @@ public interface LogicalBreakpoint { * *

* This affects the mapped program, if applicable, and all open and live traces. Note, depending - * on the debugging model, the enabled or created breakpoints may apply to more targets. + * on the debugging model, the enabled or created breakpoints may affect other targets. * *

* This simply issues the command. The logical breakpoint is updated only when the resulting @@ -809,7 +810,7 @@ public interface LogicalBreakpoint { * *

* This affects the mapped program, if applicable, and all open and live traces. Note, depending - * on the debugging model, the disabled breakpoints may apply to more targets. + * on the debugging model, the disabled breakpoints may affect other targets. * *

* This simply issues the command. The logical breakpoint is updated only when the resulting @@ -825,7 +826,7 @@ public interface LogicalBreakpoint { *

* This presumes the breakpoint's specifications are deletable. This affects the mapped program, * if applicable, and all open and live traces. Note, depending on the debugging model, the - * deleted breakpoints may be removed from more targets. + * deleted breakpoints may be removed from other targets. * *

* This simply issues the command. The logical breakpoint is updated only when the resulting diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java new file mode 100644 index 0000000000..4234bfcf62 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java @@ -0,0 +1,2693 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.debug.flatapi; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Collectors; + +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.service.emulation.ProgramEmulationUtils; +import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer; +import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer.*; +import ghidra.app.script.GhidraScript; +import ghidra.app.script.GhidraState; +import ghidra.app.services.*; +import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; +import ghidra.app.services.DebuggerStateEditingService.StateEditor; +import ghidra.dbg.AnnotatedDebuggerAttributeListener; +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; +import ghidra.dbg.target.TargetSteppable.TargetStepKind; +import ghidra.dbg.util.PathUtils; +import ghidra.program.flatapi.FlatProgramAPI; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; +import ghidra.trace.model.Trace; +import ghidra.trace.model.TraceLocation; +import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet; +import ghidra.trace.model.memory.TraceMemoryOperations; +import ghidra.trace.model.memory.TraceMemoryRegisterSpace; +import ghidra.trace.model.program.TraceProgramView; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.schedule.TraceSchedule; +import ghidra.util.MathUtilities; +import ghidra.util.Swing; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * This interface is a flattened version of the Debugger and Trace APIs. + * + *

+ * To use this "mix-in" interface, extend {@link GhidraScript} as you normally would for your + * script, but also add this interface to the {@code implements} clause of your script, e.g., + * {@code class MyDebuggerScript extends GhidraScript implements FlatDebuggerAPI}. + */ +public interface FlatDebuggerAPI { + + /** + * The method used to wait on futures. + * + *

+ * By default, this waits at most 1 minute. + * + * @param the type of the result + * @param cf the future + * @return the result + * @throws InterruptedException if execution is interrupted + * @throws ExecutionException if an error occurs + * @throws TimeoutException if the future does not complete in time + */ + default T waitOn(CompletableFuture cf) + throws InterruptedException, ExecutionException, TimeoutException { + return cf.get(1, TimeUnit.MINUTES); + } + + /** + * Get the script state + * + *

+ * This is required to get various debugger services. It should be implemented by virtue of + * extending {@link GhidraScript}. + * + * @return the state + */ + GhidraState getState(); + + /** + * Require a service from the tool + * + *

+ * If the service is missing, an exception is thrown directing the user to run the script from + * the Debugger tool. + * + * @param the type of the service + * @param cls the class of the service + * @return the service + * @throws IllegalStateException if the service is missing + */ + default T requireService(Class cls) { + T service = getState().getTool().getService(cls); + if (service == null) { + throw new IllegalStateException("Tool does not have service " + cls + + "! This script should be run from the Debugger tool"); + } + return service; + } + + /** + * Get the trace manager service + * + * @return the service + */ + default DebuggerTraceManagerService getTraceManager() { + return requireService(DebuggerTraceManagerService.class); + } + + /** + * Open the given trace in the UI + * + * @param trace the trace + */ + default void openTrace(Trace trace) { + getTraceManager().openTrace(trace); + } + + /** + * Close the given trace in the UI + * + * @param trace the trace + */ + default void closeTrace(Trace trace) { + getTraceManager().closeTrace(trace); + } + + /** + * Get the current "coordinates", i.e., trace, thread, frame, snap, etc., usually for the active + * target. + * + * @return the coordinates + */ + default DebuggerCoordinates getCurrentDebuggerCoordinates() { + return getTraceManager().getCurrent(); + } + + /** + * Get the current trace + * + * @see #getCurrentDebuggerCoordinates() + * @return the trace + */ + default Trace getCurrentTrace() { + return getTraceManager().getCurrentTrace(); + } + + /** + * Get the current trace, throwing an exception if there isn't one + * + * @return the trace + * @throws IllegalStateException if there is no current trace + */ + default Trace requireCurrentTrace() { + Trace trace = getCurrentTrace(); + if (trace == null) { + throw new IllegalStateException("There is no current trace"); + } + return trace; + } + + /** + * Require that the given trace is not null + * + * @param trace the trace + * @return the trace + * @throws IllegalStateException if the trace is null + */ + default Trace requireTrace(Trace trace) { + if (trace == null) { + throw new IllegalStateException("There is no trace"); + } + return trace; + } + + /** + * Get the current thread + * + *

+ * While uncommon, it is possible for there to be a current trace, but no current thread. + * + * @see #getCurrentDebuggerCoordinates() + * @return the thread + */ + default TraceThread getCurrentThread() { + return getTraceManager().getCurrentThread(); + } + + /** + * Get the current thread, throwing an exception if there isn't one + * + * @return the thread + * @throws IllegalStateException if there is no current thread + */ + default TraceThread requireCurrentThread() { + TraceThread thread = getCurrentThread(); + if (thread == null) { + throw new IllegalStateException("There is no current thread"); + } + return thread; + } + + /** + * Require that the given thread is not null + * + * @param thread the thread + * @return the thread + * @throws IllegalStateException if the thread is null + */ + default TraceThread requireThread(TraceThread thread) { + if (thread == null) { + throw new IllegalStateException("There is no thread"); + } + return thread; + } + + /** + * Get the current trace program view + * + *

+ * The view is an adapter for traces that allows them to be used as a {@link Program}. However, + * it only works for a chosen snapshot. Typically, {@link TraceProgramView#getSnap()} for this + * view will give the same result as {@link #getCurrentSnap()}. The exception is when the UI is + * displaying emulated (scratch) machine state. In that case, {@link #getCurrentSnap()} will + * give the "source" snapshot of the emulated state, and {@link TraceProgramView#getSnap()} will + * give the "destination" scratch snapshot. See {@link #getCurrentEmulationSchedule()}. + * + * @see #getCurrentDebuggerCoordinates() + * @return the view + */ + default TraceProgramView getCurrentView() { + return getTraceManager().getCurrentView(); + } + + /** + * Get the current trace view, throwing an exception if there isn't one + * + * @return the trace view + * @throws IllegalStateException if there is no current trace view + */ + default TraceProgramView requireCurrentView() { + TraceProgramView view = getCurrentView(); + if (view == null) { + throw new IllegalStateException("There is no current trace view"); + } + return view; + } + + /** + * Get the current frame, 0 being the innermost + * + *

+ * If the target doesn't support frames, this will return 0 + * + * @see #getCurrentDebuggerCoordinates() + * @return the frame + */ + default int getCurrentFrame() { + return getTraceManager().getCurrentFrame(); + } + + /** + * Get the current snap, i.e., snapshot key + * + *

+ * Snaps are the trace's notion of time. Positive keys should be monotonic with respect to time: + * a higher value implies a later point in time. Negative keys do not; they are used as scratch + * space, usually for displaying emulated machine states. This value defaults to 0, so it is + * only meaningful if there is a current trace. + * + * @see #getCurrentDebuggerCoordinates() + * @return the snap + */ + default long getCurrentSnap() { + return getTraceManager().getCurrentSnap(); + } + + /** + * Get the current emulation schedule + * + *

+ * This constitutes the current snapshot and an optional schedule of emulation steps. If there + * is a schedule, then the view's snap will be the destination scratch snap rather than the + * current snap. + * + * @return the emulation schedule + */ + default TraceSchedule getCurrentEmulationSchedule() { + return getTraceManager().getCurrent().getTime(); + } + + /** + * Make the given trace the active trace + * + *

+ * If the trace is not already open in the tool, it will be opened automatically + * + * @param trace the trace + */ + default void activateTrace(Trace trace) { + DebuggerTraceManagerService manager = getTraceManager(); + if (trace == null) { + manager.activateTrace(null); + return; + } + if (!manager.getOpenTraces().contains(trace)) { + manager.openTrace(trace); + } + manager.activateTrace(trace); + } + + /** + * Make the given thread the active thread + * + *

+ * if the trace is not already open in the tool, it will be opened automatically + * + * @param thread the thread + */ + default void activateThread(TraceThread thread) { + DebuggerTraceManagerService manager = getTraceManager(); + if (thread == null) { + manager.activateThread(null); + return; + } + Trace trace = thread.getTrace(); + if (!manager.getOpenTraces().contains(trace)) { + manager.openTrace(trace); + } + manager.activateThread(thread); + } + + /** + * Make the given frame the active frame + * + * @param frame the frame level, 0 being the innermost + */ + default void activateFrame(int frame) { + getTraceManager().activateFrame(frame); + } + + /** + * Make the given snapshot the active snapshot + * + *

+ * Activating negative snapshot keys is not recommended. The trace manager uses negative keys + * for emulation scratch space and will activate them indirectly as needed. + * + * @param snap the snapshot key + */ + default void activateSnap(long snap) { + getTraceManager().activateSnap(snap); + } + + /** + * Get the dynamic listing service + * + * @return the service + */ + default DebuggerListingService getDebuggerListing() { + return requireService(DebuggerListingService.class); + } + + /** + * Get the current trace program view and address + * + *

+ * This constitutes a portion of the debugger coordinates plus the current dynamic address. The + * program given by {@link ProgramLocation#getProgram()} can be safely cast to + * {@link TraceProgramView}, which should give the same result as {@link #getCurrentView()}. + * + * @return the location + */ + default ProgramLocation getCurrentDebuggerProgramLocation() { + return getDebuggerListing().getCurrentLocation(); + } + + /** + * Get the current dynamic address + * + * @return the dynamic address + */ + default Address getCurrentDebuggerAddress() { + ProgramLocation loc = getCurrentDebuggerProgramLocation(); + return loc == null ? null : loc.getAddress(); + } + + /** + * Go to the given dynamic location in the dynamic listing + * + *

+ * To "go to" a point in time, use {@link #activateSnap(long)} or + * {@link #emulate(Trace, TraceSchedule, TaskMonitor)}. + * + * @param location the location, e.g., from {@link #dynamicLocation(String)} + * @return true if successful, false otherwise + */ + default boolean goToDynamic(ProgramLocation location) { + return getDebuggerListing().goTo(location, true); + } + + /** + * Go to the given dynamic address in the dynamic listing + * + * @see #goToDynamic(ProgramLocation) + */ + default boolean goToDynamic(Address address) { + return goToDynamic(dynamicLocation(address)); + } + + /** + * Go to the given dynamic address in the dynamic listing + * + * @see #goToDynamic(ProgramLocation) + */ + default boolean goToDynamic(String addrString) { + return goToDynamic(dynamicLocation(addrString)); + } + + /** + * Get the static mapping service + * + * @return the service + */ + default DebuggerStaticMappingService getMappingService() { + return requireService(DebuggerStaticMappingService.class); + } + + /** + * Get the current program + * + *

+ * This is implemented by virtue of extending {@link FlatProgramAPI}, which is inherited via + * {@link GhidraScript}. + * + * @return the current program + */ + default Program getCurrentProgram() { + return getState().getCurrentProgram(); + } + + /** + * Get the current program, throwing an exception if there isn't one. + * + * @return the current program + * @throws IllegalStateException if there is no current program + */ + default Program requireCurrentProgram() { + Program program = getCurrentProgram(); + if (program == null) { + throw new IllegalStateException("There is no current program"); + } + return program; + } + + /** + * Translate the given static location to the corresponding dynamic location + * + *

+ * This uses the trace's static mappings (see {@link Trace#getStaticMappingManager()} and + * {@link DebuggerStaticMappingService}) to translate a static location to the corresponding + * dynamic location in the current trace. If there is no current trace or the location cannot be + * translated to the current trace, the result is null. This accommodates link-load-time + * relocation, particularly from address-space layout randomization (ASLR). + * + * @param location the static location, e.g., from {@link #staticLocation(String)} + * @return the dynamic location, or null if not translated + */ + default ProgramLocation translateStaticToDynamic(ProgramLocation location) { + DebuggerCoordinates current = getCurrentDebuggerCoordinates(); + Trace trace = requireCurrentTrace(); + TraceLocation tloc = + getMappingService().getOpenMappedLocation(trace, location, current.getSnap()); + return tloc == null ? null : new ProgramLocation(current.getView(), tloc.getAddress()); + } + + /** + * Translate the given static address to the corresponding dynamic address + * + *

+ * This does the same as {@link #translateStaticToDynamic(ProgramLocation)}, but assumes the + * address is for the current program. The returned address is for the current trace view. + * + * @param address the static address + * @return the dynamic address, or null if not translated + */ + default Address translateStaticToDynamic(Address address) { + Program program = requireCurrentProgram(); + ProgramLocation dloc = translateStaticToDynamic(new ProgramLocation(program, address)); + return dloc == null ? null : dloc.getByteAddress(); + } + + /** + * Translate the given dynamic location to the corresponding static location + * + *

+ * This does the opposite of {@link #translateStaticToDynamic(ProgramLocation)}. The resulting + * static location could be for any open program, not just the current one, since a target may + * load several images. For example, a single user-space process typically has several modules: + * the executable image and several libraries. + * + * @param location the dynamic location, e.g., from {@link #dynamicLocation(String)} + * @return the static location, or null if not translated + */ + default ProgramLocation translateDynamicToStatic(ProgramLocation location) { + return getMappingService().getStaticLocationFromDynamic(location); + } + + /** + * Translate the given dynamic address to the corresponding static address + * + *

+ * This does the same as {@link #translateDynamicToStatic(ProgramLocation)}, but assumes the + * address is for the current trace view. The returned address is for the current program. If + * there is not current view or program, or if the address cannot be translated to the current + * program, null is returned. + * + * @param address the dynamic address + * @return the static address + */ + default Address translateDynamicToStatic(Address address) { + Program program = requireCurrentProgram(); + TraceProgramView view = requireCurrentView(); + ProgramLocation sloc = translateDynamicToStatic(new ProgramLocation(view, address)); + return sloc == null ? null : sloc.getProgram() != program ? null : sloc.getByteAddress(); + } + + /** + * Get the emulation service + * + * @return the service + */ + default DebuggerEmulationService getEmulationService() { + return requireService(DebuggerEmulationService.class); + } + + /** + * Load the given program into a trace suitable for emulation in the UI, starting at the given + * address + * + *

+ * Note that the program bytes are not actually loaded into the trace. Rather a static mapping + * is generated, allowing the emulator to load bytes from the target program lazily. The trace + * is automatically loaded into the UI (trace manager). + * + * @param program the target program + * @param address the initial program counter + * @return the resulting trace + * @throws IOException if the trace cannot be created + */ + default Trace emulateLaunch(Program program, Address address) throws IOException { + Trace trace = null; + try { + trace = ProgramEmulationUtils.launchEmulationTrace(program, address, this); + DebuggerTraceManagerService traceManager = getTraceManager(); + traceManager.openTrace(trace); + traceManager.activateTrace(trace); + Swing.allowSwingToProcessEvents(); + } + finally { + if (trace != null) { + trace.release(this); + } + } + return trace; + } + + /** + * Does the same as {@link #emulateLaunch(Program, Address)}, for the current program + * + * @return the resulting trace + * @throws IOException if the trace cannot be created + */ + default Trace emulateLaunch(Address address) throws IOException { + return emulateLaunch(requireCurrentProgram(), address); + } + + /** + * Emulate the given trace as specified in the given schedule and display the result in the UI + * + * @param trace the trace + * @param time the schedule of steps + * @param monitor a monitor for the emulation + * @return true if successful + * @throws CancelledException if the user cancelled via the given monitor + */ + default boolean emulate(Trace trace, TraceSchedule time, TaskMonitor monitor) + throws CancelledException { + // Use the script's thread to perform the actual emulation + getEmulationService().emulate(trace, time, monitor); + // This should just display the cached state + getTraceManager().activateTime(time); + return true; + } + + /** + * Emulate the current trace as specified and display the result + * + * @param time the schedule of steps + * @param monitor the monitor for the emulation + * @return true if successful + * @throws CancelledException if the user cancelled via the given monitor + * @throws IllegalStateException if there is no current trace + */ + default boolean emulate(TraceSchedule time, TaskMonitor monitor) throws CancelledException { + return emulate(requireCurrentTrace(), time, monitor); + } + + /** + * Step the current trace count instructions via emulation + * + * @param count the number of instructions to step, negative to step in reverse + * @param monitor a monitor for the emulation + * @return true if successful, false otherwise + * @throws CancelledException if the user cancelled via the given monitor + * @throws IllegalStateException if there is no current trace or thread + */ + default boolean stepEmuInstruction(long count, TaskMonitor monitor) throws CancelledException { + DebuggerCoordinates current = getCurrentDebuggerCoordinates(); + Trace trace = requireCurrentTrace(); + TraceThread thread = current.getThread(); + TraceSchedule time = current.getTime(); + TraceSchedule stepped = count <= 0 + ? time.steppedBackward(trace, -count) + : time.steppedForward(requireThread(thread), count); + return emulate(trace, stepped, monitor); + } + + /** + * Step the current trace count p-code operations via emulation + * + * @param count the number of operations to step, negative to step in reverse + * @param monitor a monitor for the emulation + * @return true if successful, false otherwise + * @throws CancelledException if the user cancelled via the given monitor + */ + default boolean stepEmuPcodeOp(int count, TaskMonitor monitor) throws CancelledException { + DebuggerCoordinates current = getCurrentDebuggerCoordinates(); + Trace trace = requireCurrentTrace(); + TraceThread thread = current.getThread(); + TraceSchedule time = current.getTime(); + TraceSchedule stepped = count <= 0 + ? time.steppedPcodeBackward(-count) + : time.steppedPcodeForward(requireThread(thread), count); + return emulate(trace, stepped, monitor); + } + + /** + * Step the current trace count skipped instructions via emulation + * + *

+ * Note there's no such thing as "skipping in reverse." If a negative count is given, this will + * behave the same as {@link #stepEmuInstruction(long, TaskMonitor)}. + * + * @param count the number of instructions to skip, negative to step in reverse + * @param monitor a monitor for the emulation + * @return true if successful, false otherwise + * @throws CancelledException if the user cancelled via the given monitor + */ + default boolean skipEmuInstruction(long count, TaskMonitor monitor) throws CancelledException { + DebuggerCoordinates current = getCurrentDebuggerCoordinates(); + Trace trace = requireCurrentTrace(); + TraceThread thread = current.getThread(); + TraceSchedule time = current.getTime(); + TraceSchedule stepped = count <= 0 + ? time.steppedBackward(trace, -count) + : time.skippedForward(requireThread(thread), count); + return emulate(trace, stepped, monitor); + } + + /** + * Step the current trace count skipped p-code operations via emulation + * + *

+ * Note there's no such thing as "skipping in reverse." If a negative count is given, this will + * behave the same as {@link #stepEmuPcodeOp(int, TaskMonitor)}. + * + * @param count the number of operations to skip, negative to step in reverse + * @param monitor a monitor for the emulation + * @return true if successful, false otherwise + * @throws CancelledException if the user cancelled via the given monitor + */ + default boolean skipEmuPcodeOp(int count, TaskMonitor monitor) throws CancelledException { + DebuggerCoordinates current = getCurrentDebuggerCoordinates(); + Trace trace = requireCurrentTrace(); + TraceThread thread = current.getThread(); + TraceSchedule time = current.getTime(); + TraceSchedule stepped = count <= 0 + ? time.steppedPcodeBackward(-count) + : time.skippedPcodeForward(requireThread(thread), count); + return emulate(trace, stepped, monitor); + } + + /** + * Apply the given SLEIGH patch to the emulator + * + * @param sleigh the SLEIGH source, without terminating semicolon + * @param monitor a monitor for the emulation + * @return true if successful, false otherwise + * @throws CancelledException if the user cancelled via the given monitor + */ + default boolean patchEmu(String sleigh, TaskMonitor monitor) throws CancelledException { + DebuggerCoordinates current = getCurrentDebuggerCoordinates(); + Trace trace = requireCurrentTrace(); + TraceThread thread = current.getThread(); + TraceSchedule time = current.getTime(); + TraceSchedule patched = time.patched(requireThread(thread), sleigh); + return emulate(trace, patched, monitor); + } + + /** + * Create an address range, avoiding address overflow by truncating + * + *

+ * If the length would cause address overflow, it is adjusted such that the range's maximum + * address is the space's maximum address. + * + * @param start the minimum address + * @param length the desired length + * @return the range + */ + default AddressRange safeRange(Address start, int length) { + if (length < 0) { + throw new IllegalArgumentException("length < 0"); + } + long maxLength = start.getAddressSpace().getMaxAddress().subtract(start); + try { + return new AddressRangeImpl(start, MathUtilities.unsignedMin(length, maxLength)); + } + catch (AddressOverflowException e) { + throw new AssertionError(e); + } + } + + /** + * Copy memory from target to trace, if applicable and not already cached + * + * @param trace the trace to update + * @param snap the snap the snap, to determine whether target bytes are applicable + * @param start the starting address + * @param length the number of bytes to make fresh + * @throws InterruptedException if the operation is interrupted + * @throws ExecutionException if an error occurs + * @throws TimeoutException if the operation times out + */ + default void refreshMemoryIfLive(Trace trace, long snap, Address start, int length, + TaskMonitor monitor) throws InterruptedException, ExecutionException, TimeoutException { + TraceRecorder recorder = getModelService().getRecorder(trace); + if (recorder == null) { + return; + } + if (recorder.getSnap() != snap) { + return; + } + waitOn(recorder.readMemoryBlocks(new AddressSet(safeRange(start, length)), monitor, false)); + waitOn(recorder.getTarget().getModel().flushEvents()); + waitOn(recorder.flushTransactions()); + trace.flushEvents(); + } + + /** + * Read memory into the given buffer, refreshing from target if needed + * + * @param trace the source trace + * @param snap the source snap + * @param start the source starting address + * @param buffer the destination buffer + * @param monitor a monitor for live read progress + * @return the number of bytes read + */ + default int readMemory(Trace trace, long snap, Address start, byte[] buffer, + TaskMonitor monitor) { + try { + refreshMemoryIfLive(trace, snap, start, buffer.length, monitor); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return 0; + } + + return trace.getMemoryManager().getViewBytes(snap, start, ByteBuffer.wrap(buffer)); + } + + /** + * Read memory, refreshing from target if needed + * + * @param trace the source trace + * @param snap the source snap + * @param start the source starting address + * @param length the desired number of bytes + * @param monitor a monitor for live read progress + * @return the array of bytes read, can be shorter than desired + */ + default byte[] readMemory(Trace trace, long snap, Address start, int length, + TaskMonitor monitor) { + byte[] arr = new byte[length]; + int actual = readMemory(trace, snap, start, arr, monitor); + if (actual == length) { + return arr; + } + return Arrays.copyOf(arr, actual); + } + + /** + * Read memory from the current trace view into the given buffer, refreshing from target if + * needed + * + * @param start the starting address + * @param buffer the destination buffer + * @return the number of bytes read + */ + default int readMemory(Address start, byte[] buffer, TaskMonitor monitor) { + TraceProgramView view = requireCurrentView(); + return readMemory(view.getTrace(), view.getSnap(), start, buffer, monitor); + } + + /** + * Read memory for the current trace view, refreshing from target if needed + * + * @param start the starting address + * @param length the desired number of bytes + * @return the array of bytes read, can be shorter than desired + */ + default byte[] readMemory(Address start, int length, TaskMonitor monitor) { + TraceProgramView view = requireCurrentView(); + return readMemory(view.getTrace(), view.getSnap(), start, length, monitor); + } + + /** + * Search trace memory for a given masked byte sequence + * + *

+ * NOTE: This searches the trace only. It will not interrogate the live target. There are + * two mechanisms for searching a live target's full memory: 1) Capture the full memory (or the + * subset to search) -- using, e.g., {@link #refreshMemoryIfLive(Trace, long, Address, int)} -- + * then search the trace. 2) If possible, invoke the target debugger's search functions -- + * using, e.g., {@link #executeCapture(String)}. + * + *

+ * This delegates to + * {@link TraceMemoryOperations#findBytes(long, AddressRange, ByteBuffer, ByteBuffer, boolean, TaskMonitor)}. + * It culls out ranges that have never been recorded, effectively excluding default {@code 00}s. + * This can only search a single snapshot per invocation, but it does include stale bytes, i.e., + * those from a previous snapshot without a more up-to-date record. In particular, a stale + * {@code 00} is matched as usual, as is any stale byte. Only those ranges which have + * never been recorded are culled. While not required, memory is conventionally read + * and recorded in pages, so culling tends to occur at page boundaries. + * + *

+ * Be wary of leading or trailing wildcards, i.e., masked-out bytes. The full data array must + * fit within the given range after culling. For example, suppose the byte {@code 12} is + * recorded at {@code ram:00400000}. The full page is recorded, but the preceding page has never + * been recorded. Thus, the byte at {@code ram:003fffff} is a default {@code 00}. Searching for + * the pattern {@code ?? 12} in the range {@code ram:00400000:00400fff} will not find the match. + * This much is intuitive, because the match starts at {@code ram:003fffff}, which is outside + * the specified range. However, this rule also affects trailing wildcards. Furthermore, because + * the preceding page was never recorded, even if the specified range were + * {@code ram:003ff000:00400fff}, the range would be culled, and the match would still be + * excluded. Nothing -- not even a wildcard -- can match a default {@code 00}. + * + * @param trace the trace to search + * @param snap the snapshot of the trace to search + * @param range the range within to search + * @param data the bytes to search for + * @param mask a mask on the bits to search, or null to match exactly. + * @param forward true to start at the min address going forward, false to start at the max + * address going backward + * @param monitor a monitor for search progress + * @return the minimum address of the matched bytes, or null if not found + */ + default Address searchMemory(Trace trace, long snap, AddressRange range, ByteBuffer data, + ByteBuffer mask, boolean forward, TaskMonitor monitor) { + return trace.getMemoryManager().findBytes(snap, range, data, mask, forward, monitor); + } + + /** + * @see #searchMemory(Trace, long, AddressRange, ByteBuffer, ByteBuffer, boolean, TaskMonitor) + */ + default Address searchMemory(Trace trace, long snap, AddressRange range, byte[] data, + byte[] mask, boolean forward, TaskMonitor monitor) { + return searchMemory(trace, snap, range, ByteBuffer.wrap(data), + mask == null ? null : ByteBuffer.wrap(mask), forward, monitor); + } + + /** + * Copy registers from target to trace, if applicable and not already cached + * + * @param thread the trace thread to update + * @param frame the frame level, 0 being the innermost + * @param snap the snap, to determine whether target values are applicable + * @param registers the registers to make fresh + * @throws InterruptedException if the operation is interrupted + * @throws ExecutionException if an error occurs + * @throws TimeoutException if the operation times out + */ + default void refreshRegistersIfLive(TraceThread thread, int frame, long snap, + Collection registers) + throws InterruptedException, ExecutionException, TimeoutException { + Trace trace = thread.getTrace(); + TraceRecorder recorder = getModelService().getRecorder(trace); + if (recorder == null) { + return; + } + if (recorder.getSnap() != snap) { + return; + } + Set asSet = + registers instanceof Set ? (Set) registers : Set.copyOf(registers); + waitOn(recorder.captureThreadRegisters(thread, frame, asSet)); + waitOn(recorder.getTarget().getModel().flushEvents()); + waitOn(recorder.flushTransactions()); + trace.flushEvents(); + } + + /** + * Read several registers from the given context, refreshing from target if needed + * + * @param thread the trace thread + * @param frame the source frame level, 0 being the innermost + * @param snap the source snap + * @param registers the source registers + * @return the list of register values, or null on error + */ + default List readRegisters(TraceThread thread, int frame, long snap, + Collection registers) { + try { + refreshRegistersIfLive(thread, frame, snap, registers); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + TraceMemoryRegisterSpace regs = + thread.getTrace().getMemoryManager().getMemoryRegisterSpace(thread, frame, false); + if (regs == null) { + return registers.stream().map(RegisterValue::new).collect(Collectors.toList()); + } + return registers.stream().map(r -> regs.getValue(snap, r)).collect(Collectors.toList()); + } + + /** + * Read a register + * + * @see #readRegisters(TraceThread, int, long, Collection) + * @return the register's value, or null on error + */ + default RegisterValue readRegister(TraceThread thread, int frame, long snap, + Register register) { + List result = readRegisters(thread, frame, snap, Set.of(register)); + return result == null ? null : result.get(0); + } + + /** + * Read several registers from the current context, refreshing from the target if needed + * + * @see #readRegisters(TraceThread, int, long, Collection) + */ + default List readRegisters(Collection registers) { + DebuggerCoordinates current = getCurrentDebuggerCoordinates(); + return readRegisters(requireThread(current.getThread()), current.getFrame(), + current.getSnap(), registers); + } + + /** + * Validate and retrieve the named registers + * + * @param language the language defining the registers + * @param names the names + * @return the registers, in the same order + * @throws IllegalArgumentException if any name is invalid + */ + default List validateRegisterNames(Language language, Collection names) { + List invalid = new ArrayList<>(); + List result = new ArrayList<>(); + for (String n : names) { + Register register = language.getRegister(n); + if (register != null) { + result.add(register); + } + else { + invalid.add(n); + } + } + if (!invalid.isEmpty()) { + throw new IllegalArgumentException("One or more invalid register names: " + invalid); + } + return result; + } + + /** + * Validate and retrieve the name register + * + * @param language the language defining the register + * @param name the name + * @return the register + * @throws IllegalArgumentException if the name is invalid + */ + default Register validateRegisterName(Language language, String name) { + Register register = language.getRegister(name); + if (register == null) { + throw new IllegalArgumentException("Invalid register name: " + name); + } + return register; + } + + /** + * Read several registers from the current context, refreshing from the target if needed + * + * @see #readRegisters(TraceThread, int, long, Collection) + * @throws IllegalArgumentException if any name is invalid + */ + default List readRegistersNamed(Collection names) { + return readRegisters(validateRegisterNames(requireCurrentTrace().getBaseLanguage(), names)); + } + + /** + * Read a register from the current context, refreshing from the target if needed + * + * @param register the register + * @return the value, or null on error + */ + default RegisterValue readRegister(Register register) { + DebuggerCoordinates current = getCurrentDebuggerCoordinates(); + return readRegister(requireThread(current.getThread()), current.getFrame(), + current.getSnap(), register); + } + + /** + * Read a register from the current context, refreshing from the target if needed + * + * @see #readRegister(Register) + * @throws IllegalArgumentException if the name is invalid + */ + default RegisterValue readRegister(String name) { + return readRegister(validateRegisterName(requireCurrentTrace().getBaseLanguage(), name)); + } + + /** + * Get the program counter for the given context + * + * @param coordinates the context + * @return the program counter, or null if not known + */ + default Address getProgramCounter(DebuggerCoordinates coordinates) { + Language language = requireTrace(coordinates.getTrace()).getBaseLanguage(); + RegisterValue value = + readRegister(requireThread(coordinates.getThread()), coordinates.getFrame(), + coordinates.getSnap(), language.getProgramCounter()); + if (!value.hasValue()) { + return null; + } + return language.getDefaultSpace().getAddress(value.getUnsignedValue().longValue()); + } + + /** + * Get the program counter for the current context + * + * @return the program counter, or null if not known + */ + default Address getProgramCounter() { + return getProgramCounter(getCurrentDebuggerCoordinates()); + } + + /** + * Get the stack pointer for the given context + * + * @param coordinates the context + * @return the stack pointer, or null if not known + */ + default Address getStackPointer(DebuggerCoordinates coordinates) { + CompilerSpec cSpec = requireTrace(coordinates.getTrace()).getBaseCompilerSpec(); + RegisterValue value = + readRegister(requireThread(coordinates.getThread()), coordinates.getFrame(), + coordinates.getSnap(), cSpec.getStackPointer()); + if (!value.hasValue()) { + return null; + } + return cSpec.getStackBaseSpace().getAddress(value.getUnsignedValue().longValue()); + } + + /** + * Get the stack pointer for the current context + * + * @return the stack pointer, or null if not known + */ + default Address getStackPointer() { + return getStackPointer(getCurrentDebuggerCoordinates()); + } + + /** + * Get the state editing service + * + * @return the service + */ + default DebuggerStateEditingService getEditingService() { + return requireService(DebuggerStateEditingService.class); + } + + /** + * Set the editing mode of the given trace + * + * @param trace the trace + * @param mode the mode + */ + default void setEditingMode(Trace trace, StateEditingMode mode) { + requireService(DebuggerStateEditingService.class).setCurrentMode(trace, mode); + } + + /** + * Set the editing mode of the current trace + * + * @param mode the mode + */ + default void setEditingMode(StateEditingMode mode) { + setEditingMode(requireCurrentTrace(), mode); + } + + /** + * Create a state editor for the given context, adhering to its current editing mode + * + * @return the editor + */ + default StateEditor createStateEditor(DebuggerCoordinates coordinates) { + return getEditingService() + .createStateEditor(getTraceManager().resolveCoordinates(coordinates)); + } + + /** + * Create a state editor suitable for memory edits for the given context + * + * @param trace the trace + * @param snap the snap + * @return the editor + */ + default StateEditor createStateEditor(Trace trace, long snap) { + return getEditingService().createStateEditor(DebuggerCoordinates + .trace(trace) + .withSnap(snap)); + } + + /** + * Create a state editor suitable for register or memory edits for the given context + * + * @param thread the thread + * @param frame the frame + * @param snap the snap + * @return the editor + */ + default StateEditor createStateEditor(TraceThread thread, int frame, long snap) { + return getEditingService().createStateEditor(DebuggerCoordinates + .thread(thread) + .withSnap(snap) + .withFrame(frame)); + } + + /** + * Create a state editor for the current context, adhering to the current editing mode + * + * @return the editor + */ + default StateEditor createStateEditor() { + return createStateEditor(getCurrentDebuggerCoordinates()); + } + + /** + * Patch memory using the given editor + * + *

+ * The success or failure of this method depends on a few factors. First is the user-selected + * editing mode for the trace. See {@link #setEditingMode(StateEditingMode)}. In read-only mode, + * this will always fail. When editing traces, a write almost always succeeds. Exceptions would + * probably indicate I/O errors. When editing via emulation, a write should almost always + * succeed. Second, when editing the target, the state of the target matters. If the trace has + * no target, this will always fail. If the target is not accepting commands, e.g., because the + * target or debugger is busy, this may fail or be delayed. If the target doesn't support + * editing the given space, this will fail. Some debuggers may also deny modification due to + * permissions. + * + * @param editor the editor + * @param start the starting address + * @param data the bytes to write + * @return true if successful, false otherwise + */ + default boolean writeMemory(StateEditor editor, Address start, byte[] data) { + if (!editor.isVariableEditable(start, data.length)) { + return false; + } + try { + waitOn(editor.setVariable(start, data)); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + return true; + } + + /** + * Patch memory of the given target, according to its current editing mode + * + *

+ * If you intend to apply several patches, consider using {@link #createStateEditor(Trace,long)} + * and {@link #writeMemory(StateEditor, Address, byte[])} + * + * @param trace the trace + * @param snap the snapshot + * @param start the starting address + * @param data the bytes to write + * @return true if successful, false otherwise + */ + default boolean writeMemory(Trace trace, long snap, Address start, byte[] data) { + return writeMemory(createStateEditor(trace, snap), start, data); + } + + /** + * Patch memory of the current target, according to the current editing mode + * + *

+ * If you intend to apply several patches, consider using {@link #createStateEditor()} and + * {@link #writeMemory(StateEditor, Address, byte[])} + * + * @param start the starting address + * @param data the bytes to write + * @return true if successful, false otherwise + */ + default boolean writeMemory(Address start, byte[] data) { + return writeMemory(createStateEditor(), start, data); + } + + /** + * Patch a register using the given editor + * + *

+ * The success or failure of this methods depends on a few factors. First is the user-selected + * editing mode for the trace. See {@link #setEditingMode(StateEditingMode)}. In read-only mode, + * this will always fail. When editing traces, a write almost always succeeds. Exceptions would + * probably indicate I/O errors. When editing via emulation, a write should only fail if the + * register is not accessible to Sleigh, e.g., the context register. Second, when editing the + * target, the state of the target matters. If the trace has no target, this will always fail. + * If the target is not accepting commands, e.g., because the target or debugger is busy, this + * may fail or be delayed. If the target doesn't support editing the given register, this will + * fail. + * + * @param editor the editor + * @param rv the register value + * @return true if successful, false otherwise + */ + default boolean writeRegister(StateEditor editor, RegisterValue rv) { + if (!editor.isRegisterEditable(rv.getRegister())) { + return false; + } + try { + waitOn(editor.setRegister(rv)); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + return true; + } + + /** + * Patch a register of the given context, according to its current editing mode + * + *

+ * If you intend to apply several patches, consider using + * {@link #createStateEditor(TraceThread,int,long)} and + * {@link #writeRegister(StateEditor, RegisterValue)}. + * + * @param thread the thread + * @param frame the frame + * @param snap the snap + * @param rv the register value + * @return true if successful, false otherwise + */ + default boolean writeRegister(TraceThread thread, int frame, long snap, RegisterValue rv) { + return writeRegister(createStateEditor(thread, frame, snap), rv); + } + + /** + * Patch a register of the given context, according to its current editing mode + * + * @see #writeRegister(TraceThread, int, long, RegisterValue) + * @throws IllegalArgumentException if the register name is invalid + */ + default boolean writeRegister(TraceThread thread, int frame, long snap, String name, + BigInteger value) { + return writeRegister(thread, frame, snap, new RegisterValue( + validateRegisterName(thread.getTrace().getBaseLanguage(), name), value)); + } + + /** + * Patch a register of the current thread, according to the current editing mode + * + *

+ * If you intend to apply several patches, consider using {@link #createStateEditor()} and + * {@link #writeRegister(StateEditor, RegisterValue)}. + * + * @param mode specifies in what way to apply the patch + * @param rv the register value + * @return true if successful, false otherwise + */ + default boolean writeRegister(RegisterValue rv) { + return writeRegister(createStateEditor(), rv); + } + + /** + * Patch a register of the current thread, according to the current editing mode + * + * @see #writeRegister(RegisterValue) + * @throws IllegalArgumentException if the register name is invalid + */ + default boolean writeRegister(String name, BigInteger value) { + return writeRegister(new RegisterValue( + validateRegisterName(requireCurrentTrace().getBaseLanguage(), name), value)); + } + + /** + * Get the recorder for the current target + * + *

+ * If the current trace is not live, this returns null. + * + * @return the recorder, or null + */ + default TraceRecorder getCurrentRecorder() { + return getTraceManager().getCurrent().getRecorder(); + } + + /** + * Get the model (target) service + * + * @return the service + */ + default DebuggerModelService getModelService() { + return requireService(DebuggerModelService.class); + } + + /** + * Get offers for launching the given program + * + * @param program the program + * @return the offers + */ + default List getLaunchOffers(Program program) { + return getModelService().getProgramLaunchOffers(program).collect(Collectors.toList()); + } + + /** + * Get offers for launching the current program + * + * @return the offers + */ + default List getLaunchOffers() { + return getLaunchOffers(requireCurrentProgram()); + } + + /** + * Get the best launch offer for a program, throwing an exception if there is no offer + * + * @param program the program + * @return the offer + * @throws NoSuchElementException if there is no offer + */ + default DebuggerProgramLaunchOffer requireLaunchOffer(Program program) { + Optional offer = + getModelService().getProgramLaunchOffers(program).findFirst(); + if (offer.isEmpty()) { + throw new NoSuchElementException("No offers to launch " + program); + } + return offer.get(); + } + + /** + * Launch the given offer, overriding its command line + * + *

+ * NOTE: Most offers take a command line, but not all do. If this is used for an offer + * that does not, it's behavior is undefined. + * + *

+ * Launches are not always successful, and may in fact fail frequently, usually because of + * configuration errors or missing components on the target platform. This may leave stale + * connections and/or target debuggers, processes, etc., in strange states. Furthermore, even if + * launching the target is successful, starting the recorder may not succeed, typically because + * Ghidra cannot identify and map the target platform to a Sleigh language. This method makes no + * attempt at cleaning up partial pieces. Instead it returns those pieces in the launch result. + * If the result includes a recorder, the launch was successful. If not, the script can decide + * what to do with the other pieces. That choice depends on what is expected of the user. Can + * the user reasonable be expected to intervene and complete the launch manually? How many + * targets does the script intend to launch? How big is the mess if left partially completed? + * + * @param offer the offer (this includes the program given when asking for offers) + * @param commandLine the command-line override. If this doesn't refer to the same program as + * the offer, there may be unexpected results + * @param monitor the monitor for the launch stages + * @return the result, possibly partial + */ + default LaunchResult launch(DebuggerProgramLaunchOffer offer, String commandLine, + TaskMonitor monitor) { + try { + return waitOn(offer.launchProgram(monitor, false, new LaunchConfigurator() { + @Override + public Map configureLauncher(TargetLauncher launcher, + Map arguments, RelPrompt relPrompt) { + Map adjusted = new HashMap<>(arguments); + adjusted.put(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, commandLine); + return adjusted; + } + })); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + // TODO: This is not ideal, since it's likely partially completed + return LaunchResult.totalFailure(e); + } + } + + /** + * Launch the given offer with the default/saved arguments + * + * @see #launch(DebuggerProgramLaunchOffer, String, TaskMonitor) + */ + default LaunchResult launch(DebuggerProgramLaunchOffer offer, TaskMonitor monitor) { + try { + return waitOn(offer.launchProgram(monitor, false)); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + // TODO: This is not ideal, since it's likely partially completed + return LaunchResult.totalFailure(e); + } + } + + /** + * Launch the given program, overriding its command line + * + *

+ * This takes the best offer for the given program. The command line should invoke the given + * program. If it does not, there may be unexpected results. + * + * @see #launch(DebuggerProgramLaunchOffer, String, TaskMonitor) + */ + default LaunchResult launch(Program program, String commandLine, TaskMonitor monitor) + throws InterruptedException, ExecutionException, TimeoutException { + return launch(requireLaunchOffer(program), commandLine, monitor); + } + + /** + * Launch the given program with the default/saved arguments + * + *

+ * This takes the best offer for the given program. + * + * @see #launch(DebuggerProgramLaunchOffer, String, TaskMonitor) + */ + default LaunchResult launch(Program program, TaskMonitor monitor) + throws InterruptedException, ExecutionException, TimeoutException { + return launch(requireLaunchOffer(program), monitor); + } + + /** + * Launch the current program, overriding its command line + * + * @see #launch(Program, String, TaskMonitor) + */ + default LaunchResult launch(String commandLine, TaskMonitor monitor) + throws InterruptedException, ExecutionException, TimeoutException { + return launch(requireCurrentProgram(), commandLine, monitor); + } + + /** + * Launch the current program with the default/saved arguments + * + * @see #launch(Program, TaskMonitor) + */ + default LaunchResult launch(TaskMonitor monitor) + throws InterruptedException, ExecutionException, TimeoutException { + return launch(requireCurrentProgram(), monitor); + } + + /** + * Get the target for a given trace + * + *

+ * WARNING: This method will likely change or be removed in the future. + * + * @param trace the trace + * @return the target, or null if not alive + */ + default TargetObject getTarget(Trace trace) { + TraceRecorder recorder = getModelService().getRecorder(trace); + if (recorder == null) { + return null; + } + return recorder.getTarget(); + } + + /** + * Get the target thread for a given trace thread + * + *

+ * WARNING: This method will likely change or be removed in the future. + * + * @param thread the trace thread + * @return the target thread, or null if not alive + */ + default TargetThread getTargetThread(TraceThread thread) { + TraceRecorder recorder = getModelService().getRecorder(thread.getTrace()); + if (recorder == null) { + return null; + } + return recorder.getTargetThread(thread); + } + + /** + * Get the user focus for a given trace + * + *

+ * WARNING: This method will likely change or be removed in the future. + * + * @param trace the trace + * @return the target, or null if not alive + */ + default TargetObject getTargetFocus(Trace trace) { + TraceRecorder recorder = getModelService().getRecorder(trace); + if (recorder == null) { + return null; + } + TargetObject focus = recorder.getFocus(); + return focus != null ? focus : recorder.getTarget(); + } + + /** + * Find the most suitable object related to the given object implementing the given interface + * + *

+ * WARNING: This method will likely change or be removed in the future. + * + * @param the interface type + * @param seed the seed object + * @param iface the interface class + * @return the related interface, or null + * @throws ClassCastException if the model violated its schema wrt. the requested interface + */ + @SuppressWarnings("unchecked") + default T findInterface(TargetObject seed, Class iface) { + DebuggerObjectModel model = seed.getModel(); + List found = model + .getRootSchema() + .searchForSuitable(iface, seed.getPath()); + if (found == null) { + return null; + } + try { + Object value = waitOn(model.fetchModelValue(found)); + return (T) value; + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + } + + /** + * Find the most suitable object related to the given thread implementing the given interface + * + * @param the interface type + * @param thread the thread + * @param iface the interface class + * @return the related interface, or null + * @throws ClassCastException if the model violated its schema wrt. the requested interface + */ + default T findInterface(TraceThread thread, Class iface) { + TargetThread targetThread = getTargetThread(thread); + if (targetThread == null) { + return null; + } + return findInterface(targetThread, iface); + } + + /** + * Find the most suitable object related to the given trace's focus implementing the given + * interface + * + * @param the interface type + * @param thread the thread + * @param iface the interface class + * @return the related interface, or null + * @throws ClassCastException if the model violated its schema wrt. the requested interface + */ + default T findInterface(Trace trace, Class iface) { + TargetObject focus = getTargetFocus(trace); + if (focus == null) { + return null; + } + return findInterface(focus, iface); + } + + /** + * Find the interface related to the current thread or trace + * + *

+ * This first attempts to find the most suitable object related to the current trace thread. If + * that fails, or if there is no current thread, it tries to find the one related to the current + * trace (or its focus). If there is no current trace, this throws an exception. + * + * @param the interface type + * @param iface the interface class + * @return the related interface, or null + * @throws IllegalStateException if there is no current trace + */ + default T findInterface(Class iface) { + TraceThread thread = getCurrentThread(); + T t = thread == null ? null : findInterface(thread, iface); + if (t != null) { + return t; + } + return findInterface(requireCurrentTrace(), iface); + } + + /** + * Step the given target object + * + * @param steppable the steppable target object + * @param kind the kind of step to take + * @return true if successful, false otherwise + */ + default boolean step(TargetSteppable steppable, TargetStepKind kind) { + if (steppable == null) { + return false; + } + try { + waitOn(steppable.step(kind)); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + return true; + } + + /** + * Step the given thread on target according to the given kind + * + * @param thread the trace thread + * @param kind the kind of step to take + * @return true if successful, false otherwise + */ + default boolean step(TraceThread thread, TargetStepKind kind) { + if (thread == null) { + return false; + } + return step(findInterface(thread, TargetSteppable.class), kind); + } + + /** + * Step the current thread, stepping into subroutines + * + * @return true if successful, false otherwise + */ + default boolean stepInto() { + return step(findInterface(TargetSteppable.class), TargetStepKind.INTO); + } + + /** + * Step the current thread, stepping over subroutines + * + * @return true if successful, false otherwise + */ + default boolean stepOver() { + return step(findInterface(TargetSteppable.class), TargetStepKind.OVER); + } + + /** + * Step the current thread, until it returns from the current subroutine + * + * @return true if successful, false otherwise + */ + default boolean stepOut() { + return step(findInterface(TargetSteppable.class), TargetStepKind.FINISH); + } + + /** + * Resume execution of the given target object + * + * @param resumable the resumable target object + * @return true if successful, false otherwise + */ + default boolean resume(TargetResumable resumable) { + if (resumable == null) { + return false; + } + try { + waitOn(resumable.resume()); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + return true; + } + + /** + * Resume execution of the live target for the given trace thread + * + *

+ * This is commonly called "continue" or "go," as well. + * + * @param thread the thread + * @return true if successful, false otherwise + */ + default boolean resume(TraceThread thread) { + return resume(findInterface(thread, TargetResumable.class)); + } + + /** + * Resume execution of the live target for the given trace + * + *

+ * This is commonly called "continue" or "go," as well. + * + * @param trace the trace + * @return true if successful, false otherwise + */ + default boolean resume(Trace trace) { + return resume(findInterface(trace, TargetResumable.class)); + } + + /** + * Resume execution of the current thread or trace + * + * @return true if successful, false otherwise + */ + default boolean resume() { + TraceThread thread = getCurrentThread(); + TargetResumable resumable = + thread == null ? null : findInterface(thread, TargetResumable.class); + if (resumable == null) { + resumable = findInterface(requireCurrentTrace(), TargetResumable.class); + } + return resume(resumable); + } + + /** + * Interrupt execution of the given target object + * + * @param interruptible the interruptible target object + * @return true if successful, false otherwise + */ + default boolean interrupt(TargetInterruptible interruptible) { + if (interruptible == null) { + return false; + } + try { + waitOn(interruptible.interrupt()); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + return true; + } + + /** + * Interrupt execution of the live target for the given trace thread + * + *

+ * This is commonly called "pause" or "break," as well, but not "stop." + * + * @return true if successful, false otherwise + */ + default boolean interrupt(TraceThread thread) { + return interrupt(findInterface(thread, TargetInterruptible.class)); + } + + /** + * Interrupt execution of the live target for the given trace + * + *

+ * This is commonly called "pause" or "break," as well, but not "stop." + * + * @return true if successful, false otherwise + */ + default boolean interrupt(Trace trace) { + return interrupt(findInterface(trace, TargetInterruptible.class)); + } + + /** + * Interrupt execution of the current thread or trace + * + * @return true if successful, false otherwise + */ + default boolean interrupt() { + return interrupt(findInterface(TargetInterruptible.class)); + } + + /** + * Terminate execution of the given target object + * + * @param interruptible the interruptible target object + * @return true if successful, false otherwise + */ + default boolean kill(TargetKillable killable) { + if (killable == null) { + return false; + } + try { + waitOn(killable.kill()); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + return true; + } + + /** + * Terminate execution of the live target for the given trace thread + * + *

+ * This is commonly called "stop" as well. + * + * @return true if successful, false otherwise + */ + default boolean kill(TraceThread thread) { + return kill(findInterface(thread, TargetKillable.class)); + } + + /** + * Terminate execution of the live target for the given trace + * + *

+ * This is commonly called "stop" as well. + * + * @return true if successful, false otherwise + */ + default boolean kill(Trace trace) { + return kill(findInterface(trace, TargetKillable.class)); + } + + /** + * Terminate execution of the current thread or trace + * + * @return true if successful, false otherwise + */ + default boolean kill() { + return kill(findInterface(TargetKillable.class)); + } + + /** + * Get the current state of the given target + * + *

+ * Any invalidated object is considered {@link TargetExecutionState#TERMINATED}. Otherwise, it's + * at least considered {@link TargetExecutionState#ALIVE}. A more specific state may be + * determined by searching the model for the conventionally-related object implementing + * {@link TargetObjectStateful}. This method applies this convention. + * + * @param target the target object + * @return the target object's execution state + */ + default TargetExecutionState getExecutionState(TargetObject target) { + if (!target.isValid()) { + return TargetExecutionState.TERMINATED; + } + TargetExecutionStateful stateful = findInterface(target, TargetExecutionStateful.class); + return stateful == null ? TargetExecutionState.ALIVE : stateful.getExecutionState(); + } + + /** + * Get the current state of the given trace + * + *

+ * If the trace does not have a live target, it is considered + * {@link TargetExecutionState#TERMINATED} (even if the trace never technically had a + * live target.) Otherwise, this gets the state of that live target. NOTE: This does not + * consider the current snap. It only considers a live target in the present. + * + * @param trace the trace + * @return the trace's target's execution state + */ + default TargetExecutionState getExecutionState(Trace trace) { + TargetObject target = getTarget(trace); + if (target == null) { + return TargetExecutionState.TERMINATED; + } + return getExecutionState(target); + } + + /** + * Get the current state of the given thread + * + *

+ * If the thread does not have a corresponding live target thread, it is considered + * {@link TargetExecutionState#TERMINATED} (even if the thread never technically had a + * live target thread.) Otherwise, this gets the state of that live target thread. NOTE: + * This does not consider the current snap. It only considers a live target thread in the + * present. In other words, if the user rewinds trace history to a point where the thread was + * alive, this method still considers that thread terminated. To compute state with respect to + * trace history, use {@link TraceThread#getLifespan()} and check if it contains the current + * snap. + * + * @param thread + * @return + */ + default TargetExecutionState getExecutionState(TraceThread thread) { + TargetObject target = getTargetThread(thread); + if (target == null) { + return TargetExecutionState.TERMINATED; + } + return getExecutionState(target); + } + + /** + * Check if the given trace's target is alive + * + * @param trace the trace + * @return true if alive + */ + default boolean isTargetAlive(Trace trace) { + return getExecutionState(trace).isAlive(); + } + + /** + * Check if the current target is alive + * + *

+ * NOTE: To be "current," the target must be recorded, and its trace must be the current + * trace. + * + * @return true if alive + */ + default boolean isTargetAlive() { + return isTargetAlive(requireCurrentTrace()); + } + + /** + * Check if the given trace thread's target is alive + * + * @param thread the thread + * @return true if alive + */ + default boolean isThreadAlive(TraceThread thread) { + return getExecutionState(thread).isAlive(); + } + + /** + * Check if the current target thread is alive + * + *

+ * NOTE: To be the "current" target thread, the target must be recorded, and its trace + * thread must be the current thread. + * + * @return + */ + default boolean isThreadAlive() { + return isThreadAlive(requireThread(getCurrentThread())); + } + + /** + * Waits for the given target to exit the {@link TargetExecutionState#RUNNING} state + * + *

+ * NOTE: There may be subtleties depending on the target debugger. For the most part, if + * the connection is handling a single target, things will work as expected. However, if there + * are multiple targets on one connection, it is possible for the given target to break, but for + * the target debugger to remain unresponsive to commands. This would happen, e.g., if a second + * target on the same connection is still running. + * + * @param target the target + * @param timeout the maximum amount of time to wait + * @param unit the units for time + * @throws TimeoutException if the timeout expires + */ + default void waitForBreak(TargetObject target, long timeout, TimeUnit unit) + throws TimeoutException { + TargetExecutionStateful stateful = findInterface(target, TargetExecutionStateful.class); + if (stateful == null) { + throw new IllegalArgumentException("Given target is not stateful"); + } + var listener = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + CompletableFuture future = new CompletableFuture<>(); + + @AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) + private void stateChanged(TargetObject parent, TargetExecutionState state) { + if (parent == stateful && !state.isRunning()) { + future.complete(null); + } + } + }; + target.getModel().addModelListener(listener); + try { + if (!stateful.getExecutionState().isRunning()) { + return; + } + listener.future.get(timeout, unit); + } + catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + finally { + target.getModel().removeModelListener(listener); + } + } + + /** + * Wait for the trace's target to break + * + *

+ * If the trace has no target, this method returns immediately, i.e., it assumes the target has + * terminated. + * + * @see #waitForBreak(TargetObject, long, TimeUnit) + * @param trace the trace + * @param timeout the maximum amount of time to wait + * @param unit the units for time + * @throws TimeoutException if the timeout expires + */ + default void waitForBreak(Trace trace, long timeout, TimeUnit unit) throws TimeoutException { + TargetObject target = getTarget(trace); + if (target == null || !target.isValid()) { + return; + } + waitForBreak(target, timeout, unit); + } + + /** + * Wait for the current target to break + * + * @see #waitForBreak(Trace, long, TimeUnit) + * @param timeout the maximum + * @param unit + * @param timeout the maximum amount of time to wait + * @param unit the units for time + * @throws TimeoutException if the timeout expires + * @throws IllegalStateException if there is no current trace + */ + default void waitForBreak(long timeout, TimeUnit unit) throws TimeoutException { + waitForBreak(requireCurrentTrace(), timeout, unit); + } + + /** + * Execute a command in a connection's interpreter, capturing the output + * + *

+ * This executes a raw command in the given interpreter. The command could have arbitrary + * effects, so it may be necessary to wait for those effects to be handled by the tool's + * services and plugins before proceeding. + * + * @param interpreter the interpreter + * @param command the command + * @return the output, or null if there is no interpreter + */ + default String executeCapture(TargetInterpreter interpreter, String command) { + if (interpreter == null) { + return null; + } + try { + return waitOn(interpreter.executeCapture(command)); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + } + + /** + * Execute a command on the live debugger for the given trace, capturing the output + * + * @param trace the trace + * @param command the command + * @return the output, or null if there is no live interpreter + */ + default String executeCapture(Trace trace, String command) { + return executeCapture(findInterface(trace, TargetInterpreter.class), command); + } + + /** + * Execute a command on the live debugger for the current trace, capturing the output + * + * @param command the command + * @return the output, or null if there is no live interpreter + * @throws IllegalStateException if there is no current trace + */ + default String executeCapture(String command) { + return executeCapture(requireCurrentTrace(), command); + } + + /** + * Execute a command in a connection's interpreter + * + *

+ * This executes a raw command in the given interpreter. The command could have arbitrary + * effects, so it may be necessary to wait for those effects to be handled by the tool's + * services and plugins before proceeding. + * + * @param interpreter the interpreter + * @param command the command + * @return true if successful + */ + default boolean execute(TargetInterpreter interpreter, String command) { + if (interpreter == null) { + return false; + } + try { + waitOn(interpreter.executeCapture(command)); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + return true; + } + + /** + * Execute a command on the live debugger for the given trace + * + * @param trace the trace + * @param command the command + * @return true if successful + */ + default boolean execute(Trace trace, String command) { + return execute(findInterface(trace, TargetInterpreter.class), command); + } + + /** + * Execute a command on the live debugger for the current trace + * + * @param command the command + * @return true if successful + * @throws IllegalStateException if there is no current trace + */ + default boolean execute(String command) { + return execute(requireCurrentTrace(), command); + } + + /** + * Get the breakpoint service + * + * @return the service + */ + default DebuggerLogicalBreakpointService getBreakpointService() { + return requireService(DebuggerLogicalBreakpointService.class); + } + + /** + * Create a static location at the given address in the current program + * + * @param program the (static) program + * @param address the address + * @return the location + */ + default ProgramLocation staticLocation(Program program, Address address) { + if (program instanceof TraceProgramView) { + throw new IllegalArgumentException("The given program is dynamic, i.e., a trace view"); + } + return new ProgramLocation(program, address); + } + + /** + * Create a static location at the given address in the current program + * + * @param program the (static) program + * @param addrString the address string + * @return the location + */ + default ProgramLocation staticLocation(Program program, String addrString) { + return staticLocation(program, program.getAddressFactory().getAddress(addrString)); + } + + /** + * Create a static location at the given address in the current program + * + * @param address the address + * @return the location + */ + default ProgramLocation staticLocation(Address address) { + return staticLocation(requireCurrentProgram(), address); + } + + /** + * Create a static location at the given address in the current program + * + * @param addrString the address string + * @return the location + */ + default ProgramLocation staticLocation(String addrString) { + return staticLocation(requireCurrentProgram(), addrString); + } + + /** + * Create a dynamic location at the given address in the given view + * + * @param view the (dynamic) trace view + * @param address the address + * @return the location + */ + default ProgramLocation dynamicLocation(TraceProgramView view, Address address) { + return new ProgramLocation(view, address); + } + + /** + * Create a dynamic location at the given address in the given view + * + * @param view the (dynamic) trace view + * @param addrString the address string + * @return the location + */ + default ProgramLocation dynamicLocation(TraceProgramView view, String addrString) { + return new ProgramLocation(view, view.getAddressFactory().getAddress(addrString)); + } + + /** + * Create a dynamic location at the given address in the current trace and snap + * + * @param address the address + * @return the location + */ + default ProgramLocation dynamicLocation(Address address) { + return dynamicLocation(requireCurrentView(), address); + } + + /** + * Create a dynamic location at the given address in the current trace and snap + * + * @param addrString the address string + * @return the location + */ + default ProgramLocation dynamicLocation(String addrString) { + return dynamicLocation(requireCurrentView(), addrString); + } + + /** + * Create a dynamic location at the given address in the given trace's primary view + * + * @param trace the trace + * @param address the address + * @return the location + */ + default ProgramLocation dynamicLocation(Trace trace, Address address) { + return dynamicLocation(trace.getProgramView(), address); + } + + /** + * Create a dynamic location at the given address in the given trace's primary view + * + * @param trace the trace + * @param addrString the address string + * @return the location + */ + default ProgramLocation dynamicLocation(Trace trace, String addrString) { + return dynamicLocation(trace.getProgramView(), addrString); + } + + /** + * Create a dynamic location at the given address in the given trace at the given snap + * + * @param trace the trace + * @param snap the snap + * @param address the address + * @return the location + */ + default ProgramLocation dynamicLocation(Trace trace, long snap, Address address) { + return dynamicLocation(trace.getFixedProgramView(snap), address); + } + + /** + * Create a dynamic location at the given address in the given trace at the given snap + * + * @param trace the trace + * @param snap the snap + * @param addrString the address string + * @return the location + */ + default ProgramLocation dynamicLocation(Trace trace, long snap, String addrString) { + return dynamicLocation(trace.getFixedProgramView(snap), addrString); + } + + /** + * Get all the breakpoints + * + *

+ * This returns all logical breakpoints among all open programs and traces (targets) + * + * @return the breakpoints + */ + default Set getAllBreakpoints() { + return getBreakpointService().getAllBreakpoints(); + } + + /** + * Get the breakpoints in the given program, indexed by address + * + * @param program the program + * @return the address-breakpoint-set map + */ + default NavigableMap> getBreakpoints(Program program) { + return getBreakpointService().getBreakpoints(program); + } + + /** + * Get the breakpoints in the given trace, indexed by (dynamic) address + * + * @param program the program + * @return the address-breakpoint-set map + */ + default NavigableMap> getBreakpoints(Trace trace) { + return getBreakpointService().getBreakpoints(trace); + } + + /** + * Get the breakpoints at a given location + * + * @param location the location, e.g., from {@link #staticLocation(String)} and + * {@link #dynamicLocation(String)}. + * @return the (possibly empty) set of breakpoints at that location + */ + default Set getBreakpointsAt(ProgramLocation location) { + return getBreakpointService().getBreakpointsAt(location); + } + + /** + * Get the breakpoints having the given name (from any open program or trace) + * + * @param name the name + * @return the breakpoints + */ + default Set getBreakpointsNamed(String name) { + return getBreakpointService().getAllBreakpoints() + .stream() + .filter(bp -> name.equals(bp.getName())) + .collect(Collectors.toSet()); + } + + /** + * Class that implements {@link FlatDebuggerAPI#expectBreakpointChanges()} + */ + public static class ExpectingBreakpointChanges implements AutoCloseable { + private final FlatDebuggerAPI flat; + private final CompletableFuture changesSettled; + + public ExpectingBreakpointChanges(FlatDebuggerAPI flat, + DebuggerLogicalBreakpointService service) { + this.flat = flat; + this.changesSettled = service.changesSettled(); + } + + @Override + public void close() throws InterruptedException, ExecutionException, TimeoutException { + Swing.allowSwingToProcessEvents(); + flat.waitOn(changesSettled); + } + } + + /** + * Perform some operations expected to cause changes, and then wait for those changes to settle + * + *

+ * Use this via a try-with-resources block containing the operations causing changes. + */ + default ExpectingBreakpointChanges expectBreakpointChanges() { + return new ExpectingBreakpointChanges(this, getBreakpointService()); + } + + /** + * Toggle the breakpoints at a given location + * + * @param location the location, e.g., from {@link #staticLocation(String)} and + * {@link #dynamicLocation(String)}. + * @return the (possibly empty) set of breakpoints at that location, or null if failed + */ + default Set breakpointsToggle(ProgramLocation location) { + DebuggerLogicalBreakpointService service = getBreakpointService(); + try (ExpectingBreakpointChanges exp = expectBreakpointChanges()) { + return waitOn(service.toggleBreakpointsAt(location, + () -> CompletableFuture.completedFuture(Set.of()))); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + } + + /** + * Set a breakpoint at the given location + * + *

+ * NOTE: Many asynchronous events take place when creating a breakpoint, esp., among + * several live targets. Furthermore, some targets may adjust the breakpoint specification just + * slightly. This method does its best to identify the resulting breakpoint(s) once things have + * settled. Namely, it retrieves breakpoints at the specific location having the specified name + * and assumes those are the result. It is possible this command succeeds, but this method fails + * to identify the result. In that case, the returned result will be the empty set. + * + * @param location the location, e.g., from {@link #staticLocation(String)} and + * {@link #dynamicLocation(String)}. + * @param length the length, for "access breakpoints" or "watchpoints" + * @param kinds the kinds, not all combinations are reasonable + * @param name a user-defined name + * @return the resulting breakpoint(s), or null if failed + */ + default Set breakpointSet(ProgramLocation location, long length, + TraceBreakpointKindSet kinds, String name) { + DebuggerLogicalBreakpointService service = getBreakpointService(); + try (ExpectingBreakpointChanges exp = expectBreakpointChanges()) { + waitOn(service.placeBreakpointAt(location, length, kinds, name)); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + return service.getBreakpointsAt(location) + .stream() + .filter(b -> Objects.equals(name, b.getName())) + .collect(Collectors.toSet()); + } + + /** + * Set a software breakpoint at the given location + * + * @param location the location, e.g., from {@link #staticLocation(String)} and + * {@link #dynamicLocation(String)}. + * @param name a user-defined name + * @return true if successful + */ + default Set breakpointSetSoftwareExecute(ProgramLocation location, + String name) { + return breakpointSet(location, 1, TraceBreakpointKindSet.SW_EXECUTE, name); + } + + /** + * Set a hardware breakpoint at the given location + * + * @param location the location, e.g., from {@link #staticLocation(String)} and + * {@link #dynamicLocation(String)}. + * @param name a user-defined name + * @return true if successful + */ + default Set breakpointSetHardwareExecute(ProgramLocation location, + String name) { + return breakpointSet(location, 1, TraceBreakpointKindSet.HW_EXECUTE, name); + } + + /** + * Set a read breakpoint at the given location + * + *

+ * This might also be called a "read watchpoint" or a "read access breakpoint." + * + * @param location the location, e.g., from {@link #staticLocation(String)} and + * {@link #dynamicLocation(String)}. + * @param length the length + * @param name a user-defined name + * @return true if successful + */ + default Set breakpointSetRead(ProgramLocation location, int length, + String name) { + return breakpointSet(location, length, TraceBreakpointKindSet.READ, name); + } + + /** + * Set a write breakpoint at the given location + * + *

+ * This might also be called a "write watchpoint" or a "write access breakpoint." + * + * @param location the location, e.g., from {@link #staticLocation(String)} and + * {@link #dynamicLocation(String)}. + * @param length the length + * @param name a user-defined name + * @return true if successful + */ + default Set breakpointSetWrite(ProgramLocation location, int length, + String name) { + return breakpointSet(location, length, TraceBreakpointKindSet.WRITE, name); + } + + /** + * Set an access breakpoint at the given location + * + *

+ * This might also be called a "watchpoint." + * + * @param location the location, e.g., from {@link #staticLocation(String)} and + * {@link #dynamicLocation(String)}. + * @param length the length + * @param name a user-defined name + * @return true if successful + */ + default Set breakpointSetAccess(ProgramLocation location, int length, + String name) { + return breakpointSet(location, length, TraceBreakpointKindSet.ACCESS, name); + } + + /** + * If the location is dynamic, get its trace + * + * @param location the location + * @return the trace, or null if a static location + */ + default Trace getTrace(ProgramLocation location) { + Program program = location.getProgram(); + if (program instanceof TraceProgramView) { + return ((TraceProgramView) program).getTrace(); + } + return null; + } + + /** + * Enable the breakpoints at a given location + * + * @param location the location, can be static or dynamic + * @return the (possibly empty) set of breakpoints at that location, or null if failed + */ + default Set breakpointsEnable(ProgramLocation location) { + DebuggerLogicalBreakpointService service = getBreakpointService(); + Set col = service.getBreakpointsAt(location); + try (ExpectingBreakpointChanges exp = expectBreakpointChanges()) { + waitOn(service.enableAll(col, getTrace(location))); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + return col; + } + + /** + * Disable the breakpoints at a given location + * + * @param location the location, can be static or dynamic + * @return the (possibly empty) set of breakpoints at that location, or null if failed + */ + default Set breakpointsDisable(ProgramLocation location) { + DebuggerLogicalBreakpointService service = getBreakpointService(); + Set col = service.getBreakpointsAt(location); + try (ExpectingBreakpointChanges exp = expectBreakpointChanges()) { + waitOn(service.disableAll(col, getTrace(location))); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + return col; + } + + /** + * Clear the breakpoints at a given location + * + * @param location the location, can be static or dynamic + * @return true if successful, false otherwise + */ + default boolean breakpointsClear(ProgramLocation location) { + DebuggerLogicalBreakpointService service = getBreakpointService(); + Set col = service.getBreakpointsAt(location); + try (ExpectingBreakpointChanges exp = expectBreakpointChanges()) { + waitOn(service.deleteAll(col, getTrace(location))); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + return true; + } + + /** + * Get the value at the given path for the given model + * + * @param model the model + * @param path the path + * @return the avlue, or null if the trace is not live or if the path does not exist + */ + default Object getModelValue(DebuggerObjectModel model, String path) { + try { + return waitOn(model.fetchModelValue(PathUtils.parse(path))); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + } + + /** + * Get the value at the given path for the current trace's model + * + * @param path the path + * @return the value, or null if the trace is not live or if the path does not exist + */ + default Object getModelValue(String path) { + TraceRecorder recorder = getModelService().getRecorder(getCurrentTrace()); + if (recorder == null) { + return null; + } + return getModelValue(recorder.getTarget().getModel(), path); + } + + /** + * Refresh the given objects children (elements and attributes) + * + * @param object the object + * @return the set of children, excluding primitive-valued attributes + */ + default Set refreshObjectChildren(TargetObject object) { + try { + // Refresh both children and memory/register values + waitOn(object.invalidateCaches()); + waitOn(object.resync()); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + Set result = new LinkedHashSet<>(); + result.addAll(object.getCachedElements().values()); + for (Object v : object.getCachedAttributes().values()) { + if (v instanceof TargetObject) { + result.add((TargetObject) v); + } + } + return result; + } + + /** + * Refresh the given object and its children, recursively + * + *

+ * The objects are traversed in depth-first pre-order. Links are traversed, even if the object + * is not part of the specified subtree, but an object is skipped if it has already been + * visited. + * + * @param object the seed object + * @return true if the traversal completed successfully + */ + default boolean refreshSubtree(TargetObject object) { + var util = new Object() { + Set visited = new HashSet<>(); + + boolean visit(TargetObject object) { + if (!visited.add(object)) { + return true; + } + for (TargetObject child : refreshObjectChildren(object)) { + if (!visit(child)) { + return false; + } + } + return true; + } + }; + return util.visit(object); + } + + /** + * Flush each stage of the asynchronous processing pipelines from end to end + * + *

+ * This method includes as many components as its author knows to flush. If the given trace is + * alive, flushing starts with the connection's event queue, followed by the recorder's event + * and transaction queues. Next, it flushes the trace's event queue. Then, it waits for various + * services' changes to settle, in dependency order. Currently, that is the static mapping + * service followed by the logical breakpoint service. Note that some stages use timeouts. It's + * also possible the target had not generated all the expected events by the time this method + * began flushing its queue. Thus, callers should still check that some expected condition is + * met and possibly repeat the flush before proceeding. + * + *

+ * There are additional dependents, e.g., the breakpoint listing plugin; however, scripts should + * not depend on them, so we do not wait on them. + * + * @param trace the trace whose events need to be completely processed before continuing. + * @return + */ + default boolean flushAsyncPipelines(Trace trace) { + try { + TraceRecorder recorder = getModelService().getRecorder(trace); + if (recorder != null) { + waitOn(recorder.getTarget().getModel().flushEvents()); + waitOn(recorder.flushTransactions()); + } + trace.flushEvents(); + waitOn(getMappingService().changesSettled()); + waitOn(getBreakpointService().changesSettled()); + Swing.allowSwingToProcessEvents(); + return true; + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + } + + // TODO: Interaction with the target process itself, e.g., via stdio. + // The DebugModel API does not currently support this. +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/TestDebuggerProgramLaunchOpinion.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/TestDebuggerProgramLaunchOpinion.java index 27b6baab6a..9be7cd3a34 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/TestDebuggerProgramLaunchOpinion.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/TestDebuggerProgramLaunchOpinion.java @@ -25,7 +25,6 @@ import generic.Unique; import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer; import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOpinion; import ghidra.app.services.DebuggerModelService; -import ghidra.async.AsyncUtils; import ghidra.dbg.DebuggerModelFactory; import ghidra.dbg.model.TestDebuggerModelFactory; import ghidra.framework.plugintool.PluginTool; @@ -34,10 +33,12 @@ import ghidra.util.task.TaskMonitor; public class TestDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpinion { - static class TestDebuggerProgramLaunchOffer implements DebuggerProgramLaunchOffer { + public static class TestDebuggerProgramLaunchOffer implements DebuggerProgramLaunchOffer { @Override - public CompletableFuture launchProgram(TaskMonitor monitor, boolean prompt) { - return AsyncUtils.NIL; + public CompletableFuture launchProgram(TaskMonitor monitor, boolean prompt, + LaunchConfigurator configurator) { + return CompletableFuture + .completedFuture(LaunchResult.totalFailure(new AssertionError())); } @Override diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorderTest.java index 66ae949b9a..d8c1bba217 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorderTest.java @@ -15,7 +15,7 @@ */ package ghidra.app.plugin.core.debug.service.model.record; -import static org.hamcrest.Matchers.isOneOf; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import java.math.BigInteger; @@ -304,7 +304,7 @@ public class ObjectBasedTraceRecorderTest extends AbstractGhidraHeadedDebuggerGU mb.testProcess1.memory.setMemory(tb.addr(0x00400123), mb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9)); flushAndWait(); assertThat(memory.getState(recorder.getSnap(), tb.addr(0x00400123)), - isOneOf(null, TraceMemoryState.UNKNOWN)); + is(oneOf(null, TraceMemoryState.UNKNOWN))); byte[] data = new byte[10]; waitOn(recorder.readMemory(tb.addr(0x00400123), 10)); @@ -331,7 +331,7 @@ public class ObjectBasedTraceRecorderTest extends AbstractGhidraHeadedDebuggerGU mb.testProcess1.memory.setMemory(tb.addr(0x00400123), mb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9)); flushAndWait(); assertThat(memory.getState(recorder.getSnap(), tb.addr(0x00400123)), - isOneOf(null, TraceMemoryState.UNKNOWN)); + is(oneOf(null, TraceMemoryState.UNKNOWN))); byte[] data = new byte[10]; assertNull(waitOn(recorder.readMemoryBlocks( @@ -354,7 +354,7 @@ public class ObjectBasedTraceRecorderTest extends AbstractGhidraHeadedDebuggerGU mb.testProcess1.memory.addRegion("exe:.text", mb.rng(0x00400000, 0x00400fff), "rwx"); flushAndWait(); assertThat(memory.getState(recorder.getSnap(), tb.addr(0x00400123)), - isOneOf(null, TraceMemoryState.UNKNOWN)); + is(oneOf(null, TraceMemoryState.UNKNOWN))); byte[] data = new byte[10]; waitOn(recorder.writeMemory(tb.addr(0x00400123), tb.arr(1, 2, 3, 4, 5, 6, 7, 8, 9))); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/debug/flatapi/FlatDebuggerAPITest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/debug/flatapi/FlatDebuggerAPITest.java new file mode 100644 index 0000000000..3238b1224b --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/debug/flatapi/FlatDebuggerAPITest.java @@ -0,0 +1,1275 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.debug.flatapi; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.Range; + +import generic.Unique; +import ghidra.app.plugin.assembler.Assembler; +import ghidra.app.plugin.assembler.Assemblers; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; +import ghidra.app.plugin.core.debug.service.breakpoint.DebuggerLogicalBreakpointServicePlugin; +import ghidra.app.plugin.core.debug.service.editing.DebuggerStateEditingServicePlugin; +import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePlugin; +import ghidra.app.plugin.core.debug.service.model.TestDebuggerProgramLaunchOpinion.TestDebuggerProgramLaunchOffer; +import ghidra.app.plugin.core.debug.service.model.launch.AbstractDebuggerProgramLaunchOffer; +import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer; +import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer.LaunchResult; +import ghidra.app.script.GhidraState; +import ghidra.app.services.*; +import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; +import ghidra.app.services.LogicalBreakpoint.State; +import ghidra.dbg.DebuggerModelFactory; +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.model.*; +import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetSteppable.TargetStepKind; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.Program; +import ghidra.trace.database.memory.DBTraceMemoryManager; +import ghidra.trace.database.memory.DBTraceMemoryRegisterSpace; +import ghidra.trace.model.Trace; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet; +import ghidra.trace.model.memory.TraceMemoryFlag; +import ghidra.trace.model.stack.TraceStack; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.schedule.TraceSchedule; +import ghidra.util.database.UndoableTransaction; + +public class FlatDebuggerAPITest extends AbstractGhidraHeadedDebuggerGUITest { + + protected static class TestFactory implements DebuggerModelFactory { + private final DebuggerObjectModel model; + + public TestFactory(DebuggerObjectModel model) { + this.model = model; + } + + @Override + public CompletableFuture build() { + return CompletableFuture.completedFuture(model); + } + } + + protected class TestOffer extends AbstractDebuggerProgramLaunchOffer { + public TestOffer(Program program, DebuggerModelFactory factory) { + super(program, env.getTool(), factory); + } + + public TestOffer(Program program, DebuggerObjectModel model) { + this(program, new TestFactory(model)); + } + + @Override + public String getConfigName() { + return "TEST"; + } + + @Override + public String getMenuTitle() { + return "in Test Debugger"; + } + } + + protected static class TestModelBuilder extends TestDebuggerModelBuilder { + private final TestDebuggerObjectModel model; + + public TestModelBuilder(TestDebuggerObjectModel model) { + this.model = model; + } + + @Override + protected TestDebuggerObjectModel newModel(String typeHint) { + return model; + } + } + + protected class TestFlatAPI implements FlatDebuggerAPI { + protected final GhidraState state = + new GhidraState(env.getTool(), env.getProject(), program, null, null, null); + + @Override + public GhidraState getState() { + return state; + } + } + + protected DebuggerLogicalBreakpointService breakpointService; + protected DebuggerStaticMappingService mappingService; + protected DebuggerEmulationService emulationService; + protected DebuggerListingService listingService; + protected DebuggerStateEditingService editingService; + protected FlatDebuggerAPI flat; + + @Before + public void setUpFlatAPITest() throws Throwable { + breakpointService = addPlugin(tool, DebuggerLogicalBreakpointServicePlugin.class); + mappingService = tool.getService(DebuggerStaticMappingService.class); + emulationService = addPlugin(tool, DebuggerEmulationServicePlugin.class); + listingService = addPlugin(tool, DebuggerListingPlugin.class); + editingService = addPlugin(tool, DebuggerStateEditingServicePlugin.class); + flat = new TestFlatAPI(); + } + + @Test + public void testRequireService() throws Throwable { + assertEquals(modelService, flat.requireService(DebuggerModelService.class)); + } + + interface NoSuchService { + } + + @Test(expected = IllegalStateException.class) + public void testRequireServiceAbsentErr() { + flat.requireService(NoSuchService.class); + } + + @Test + public void testGetCurrentDebuggerCoordinates() throws Throwable { + assertEquals(DebuggerCoordinates.NOWHERE, flat.getCurrentDebuggerCoordinates()); + + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + + assertEquals(DebuggerCoordinates.all(tb.trace, null, null, tb.trace.getProgramView(), + TraceSchedule.parse("0"), 0), flat.getCurrentDebuggerCoordinates()); + } + + @Test + public void testGetCurrentTrace() throws Throwable { + assertNull(flat.getCurrentTrace()); + + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + + assertEquals(tb.trace, flat.getCurrentTrace()); + } + + @Test(expected = IllegalStateException.class) + public void testRequireCurrentTraceAbsentErr() { + flat.requireCurrentTrace(); + } + + @Test + public void testGetCurrentThread() throws Throwable { + assertNull(flat.getCurrentThread()); + + createAndOpenTrace(); + TraceThread thread; + try (UndoableTransaction tid = tb.startTransaction()) { + thread = tb.getOrAddThread("Threads[0]", 0); + } + waitForSwing(); + traceManager.activateTrace(tb.trace); + + assertEquals(thread, flat.getCurrentThread()); + } + + @Test + public void testGetCurrentView() throws Throwable { + assertNull(flat.getCurrentView()); + + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + + assertEquals(tb.trace.getProgramView(), flat.getCurrentView()); + } + + @Test(expected = IllegalStateException.class) + public void testRequireCurrentViewAbsentErr() { + flat.requireCurrentView(); + } + + @Test + public void testGetCurrentFrame() throws Throwable { + assertEquals(0, flat.getCurrentFrame()); + + createAndOpenTrace(); + TraceThread thread; + try (UndoableTransaction tid = tb.startTransaction()) { + thread = tb.getOrAddThread("Threads[0]", 0); + TraceStack stack = tb.trace.getStackManager().getStack(thread, 0, true); + stack.setDepth(3, true); + } + waitForSwing(); + traceManager.activateThread(thread); + traceManager.activateFrame(1); + + assertEquals(1, flat.getCurrentFrame()); + } + + @Test + public void testGetCurrentSnap() throws Throwable { + assertEquals(0L, flat.getCurrentSnap()); + + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + traceManager.activateSnap(1); + + assertEquals(1L, flat.getCurrentSnap()); + } + + @Test + public void testGetCurrentEmulationSchedule() throws Throwable { + assertEquals(TraceSchedule.parse("0"), flat.getCurrentEmulationSchedule()); + + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + traceManager.activateSnap(1); + + assertEquals(TraceSchedule.parse("1"), flat.getCurrentEmulationSchedule()); + } + + @Test + public void testActivateTrace() throws Throwable { + createAndOpenTrace(); + flat.activateTrace(tb.trace); + + assertEquals(tb.trace, traceManager.getCurrentTrace()); + } + + @Test + public void testActivateTraceNull() throws Throwable { + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + assertEquals(tb.trace, traceManager.getCurrentTrace()); + + flat.activateTrace(null); + assertEquals(null, traceManager.getCurrentTrace()); + } + + @Test + public void testActivateTraceNotOpen() throws Throwable { + createTrace(); + assertFalse(traceManager.getOpenTraces().contains(tb.trace)); + + flat.activateTrace(tb.trace); + + assertTrue(traceManager.getOpenTraces().contains(tb.trace)); + assertEquals(tb.trace, traceManager.getCurrentTrace()); + } + + protected TraceThread createTraceWithThreadAndStack(boolean open) throws Throwable { + if (open) { + createAndOpenTrace(); + } + else { + createTrace(); + } + TraceThread thread; + try (UndoableTransaction tid = tb.startTransaction()) { + thread = tb.getOrAddThread("Threads[0]", 0); + TraceStack stack = tb.trace.getStackManager().getStack(thread, 0, true); + stack.setDepth(3, true); + } + waitForSwing(); + return thread; + } + + @Test + public void testActivateThread() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + flat.activateThread(thread); + + assertEquals(thread, traceManager.getCurrentThread()); + } + + @Test + public void testActivateThreadNull() throws Throwable { + flat.activateThread(null); + assertEquals(null, traceManager.getCurrentThread()); + + TraceThread thread = createTraceWithThreadAndStack(true); + traceManager.activateThread(thread); + waitForSwing(); + assertEquals(thread, traceManager.getCurrentThread()); + + flat.activateThread(null); // Ignored by manager + assertEquals(thread, traceManager.getCurrentThread()); + } + + @Test + public void testActivateThreadNotOpen() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(false); + assertFalse(traceManager.getOpenTraces().contains(tb.trace)); + + flat.activateThread(thread); + + assertTrue(traceManager.getOpenTraces().contains(tb.trace)); + assertEquals(thread, traceManager.getCurrentThread()); + } + + @Test + public void testActivateFrame() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + traceManager.activateThread(thread); + waitForSwing(); + flat.activateFrame(1); + + assertEquals(1, traceManager.getCurrentFrame()); + } + + @Test + public void testActivateSnap() throws Throwable { + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + flat.activateSnap(1); + + assertEquals(1L, traceManager.getCurrentSnap()); + } + + protected void createTraceWithBinText() throws Throwable { + createAndOpenTrace(); + + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceMemoryManager mm = tb.trace.getMemoryManager(); + mm.createRegion("Memory[bin.text]", 0, tb.range(0x00400000, 0x0040ffff), + Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); + + mm.putBytes(0, tb.addr(0x00400000), tb.buf(1, 2, 3, 4, 5, 6, 7, 8)); + } + traceManager.activateTrace(tb.trace); + waitForSwing(); + } + + @Test + public void testGetCurrentDebuggerAddress() throws Throwable { + assertEquals(null, flat.getCurrentDebuggerAddress()); + + createTraceWithBinText(); + + assertEquals(tb.addr(0x00400000), flat.getCurrentDebuggerAddress()); + } + + @Test + public void testGoToDynamic() throws Throwable { + createTraceWithBinText(); + + assertTrue(flat.goToDynamic("00400123")); + assertEquals(tb.addr(0x00400123), listingService.getCurrentLocation().getAddress()); + + assertTrue(flat.goToDynamic(tb.addr(0x00400321))); + assertEquals(tb.addr(0x00400321), listingService.getCurrentLocation().getAddress()); + } + + @Override + protected void createProgram(Language lang, CompilerSpec cSpec) throws IOException { + super.createProgram(lang, cSpec); + flat.getState().setCurrentProgram(program); + } + + protected void createMappedTraceAndProgram() throws Throwable { + createAndOpenTrace(); + createProgramFromTrace(); + + intoProject(program); + intoProject(tb.trace); + + programManager.openProgram(program); + traceManager.activateTrace(tb.trace); + + try (UndoableTransaction tid = UndoableTransaction.start(program, "add block", true)) { + program.getMemory() + .createInitializedBlock(".text", addr(program, 0x00400000), 4096, (byte) 0, + monitor, false); + } + + CompletableFuture changesSettled; + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getMemoryManager() + .createRegion("Memory[bin.text]", 0, tb.range(0x00400000, 0x00400fff), + Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); + changesSettled = mappingService.changesSettled(); + mappingService.addIdentityMapping(tb.trace, program, Range.atLeast(0L), true); + } + waitForSwing(); + waitOn(changesSettled); + } + + @Test + public void testGetCurrentProgram() throws Throwable { + assertEquals(null, flat.getCurrentProgram()); + + createProgram(); + programManager.openProgram(program); + + assertEquals(program, flat.getCurrentProgram()); + } + + @Test(expected = IllegalStateException.class) + public void testRequireCurrentProgramAbsentErr() throws Throwable { + flat.requireCurrentProgram(); + } + + @Test + public void testTranslateStaticToDynamic() throws Throwable { + createMappedTraceAndProgram(); + + assertEquals(flat.dynamicLocation("00400123"), + flat.translateStaticToDynamic(flat.staticLocation("00400123"))); + assertNull(flat.translateStaticToDynamic(flat.staticLocation("00600123"))); + + assertEquals(tb.addr(0x00400123), flat.translateStaticToDynamic(addr(program, 0x00400123))); + assertNull(flat.translateStaticToDynamic(addr(program, 0x00600123))); + } + + @Test + public void testTranslateDynamicToStatic() throws Throwable { + createMappedTraceAndProgram(); + + assertEquals(flat.staticLocation("00400123"), + flat.translateDynamicToStatic(flat.dynamicLocation("00400123"))); + assertNull(flat.translateDynamicToStatic(flat.dynamicLocation("00600123"))); + + assertEquals(addr(program, 0x00400123), flat.translateDynamicToStatic(tb.addr(0x00400123))); + assertNull(flat.translateDynamicToStatic(tb.addr(0x00600123))); + } + + protected Address createEmulatableProgram() throws Throwable { + createProgram(); + programManager.openProgram(program); + + Address entry = addr(program, 0x00400000); + try (UndoableTransaction start = UndoableTransaction.start(program, "init", true)) { + program.getMemory() + .createInitializedBlock(".text", entry, 4096, (byte) 0, + monitor, false); + Assembler asm = Assemblers.getAssembler(program); + asm.assemble(entry, "imm r0,#123"); + } + + // Emulate launch will create a static mapping + intoProject(program); + + return entry; + } + + @Test + public void testEmulateLaunch() throws Throwable { + Address entry = createEmulatableProgram(); + + Trace trace = flat.emulateLaunch(entry); + assertEquals(trace, traceManager.getCurrentTrace()); + } + + @Test + public void testEmulate() throws Throwable { + Address entry = createEmulatableProgram(); + + flat.emulateLaunch(entry); + flat.emulate(TraceSchedule.parse("0:t0-1"), monitor); + + assertEquals(TraceSchedule.parse("0:t0-1"), traceManager.getCurrent().getTime()); + } + + @Test + public void testStepEmuInstruction() throws Throwable { + Address entry = createEmulatableProgram(); + + flat.emulateLaunch(entry); + + flat.stepEmuInstruction(1, monitor); + assertEquals(TraceSchedule.parse("0:t0-1"), traceManager.getCurrent().getTime()); + + flat.stepEmuInstruction(-1, monitor); + assertEquals(TraceSchedule.parse("0"), traceManager.getCurrent().getTime()); + } + + @Test + public void testStepEmuPcodeOp() throws Throwable { + Address entry = createEmulatableProgram(); + + flat.emulateLaunch(entry); + + flat.stepEmuPcodeOp(1, monitor); + assertEquals(TraceSchedule.parse("0:.t0-1"), traceManager.getCurrent().getTime()); + + flat.stepEmuPcodeOp(-1, monitor); + assertEquals(TraceSchedule.parse("0"), traceManager.getCurrent().getTime()); + } + + @Test + public void testSkipEmuInstruction() throws Throwable { + Address entry = createEmulatableProgram(); + + flat.emulateLaunch(entry); + + flat.skipEmuInstruction(1, monitor); + assertEquals(TraceSchedule.parse("0:t0-s1"), traceManager.getCurrent().getTime()); + + flat.skipEmuInstruction(-1, monitor); + assertEquals(TraceSchedule.parse("0"), traceManager.getCurrent().getTime()); + } + + @Test + public void testSkipEmuPcodeOp() throws Throwable { + Address entry = createEmulatableProgram(); + + flat.emulateLaunch(entry); + + flat.skipEmuPcodeOp(1, monitor); + assertEquals(TraceSchedule.parse("0:.t0-s1"), traceManager.getCurrent().getTime()); + + flat.skipEmuPcodeOp(-1, monitor); + assertEquals(TraceSchedule.parse("0"), traceManager.getCurrent().getTime()); + } + + @Test + public void testPatchEmu() throws Throwable { + Address entry = createEmulatableProgram(); + + flat.emulateLaunch(entry); + + flat.patchEmu("r0=0x321", monitor); + assertEquals(TraceSchedule.parse("0:t0-{r0=0x321}"), traceManager.getCurrent().getTime()); + + flat.stepEmuInstruction(-1, monitor); + assertEquals(TraceSchedule.parse("0"), traceManager.getCurrent().getTime()); + } + + @Test + public void testReadMemoryBuffer() throws Throwable { + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + + byte[] data = new byte[1024]; + assertEquals(1024, flat.readMemory(tb.addr(0x00400000), data, monitor)); + assertArrayEquals(new byte[1024], data); + } + + @Test + public void testReadMemoryLength() throws Throwable { + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + + byte[] data = flat.readMemory(tb.addr(0x00400000), 1024, monitor); + assertArrayEquals(new byte[1024], data); + } + + @Test + public void testReadLiveMemory() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + mb.testProcess1.memory.writeMemory(mb.addr(0x00400000), mb.arr(1, 2, 3, 4, 5, 6, 7, 8)); + waitOn(mb.testModel.flushEvents()); + TraceRecorder recorder = record(mb.testProcess1); + waitRecorder(recorder); + useTrace(recorder.getTrace()); + waitForSwing(); + + byte[] data = flat.readMemory(tb.addr(0x00400000), 8, monitor); + assertArrayEquals(tb.arr(1, 2, 3, 4, 5, 6, 7, 8), data); + } + + @Test + public void testSearchMemory() throws Throwable { + createTraceWithBinText(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertEquals(tb.addr(0x00400003), flat.searchMemory(tb.trace, 2, tb.range(0L, -1L), + tb.arr(4, 5, 6, 7), null, true, monitor)); + assertEquals(tb.addr(0x00400003), flat.searchMemory(tb.trace, 2, tb.range(0L, -1L), + tb.arr(4, 5, 6, 7), tb.arr(-1, -1, -1, -1), true, monitor)); + } + + @Test + public void testReadRegister() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + traceManager.activateThread(thread); + + Register r0 = tb.language.getRegister("r0"); + assertEquals(new RegisterValue(r0), flat.readRegister("r0")); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadRegisterInvalidNameErr() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + traceManager.activateThread(thread); + + flat.readRegister("THERE_IS_NO_SUCH_REGISTER"); + } + + @Test + public void testReadRegisters() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + traceManager.activateThread(thread); + waitForSwing(); + + Register r0 = tb.language.getRegister("r0"); + Register r1 = tb.language.getRegister("r1"); + assertEquals(List.of( + new RegisterValue(r0), + new RegisterValue(r1)), + flat.readRegistersNamed(List.of("r0", "r1"))); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadRegistersInvalidNameErr() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + traceManager.activateThread(thread); + + flat.readRegistersNamed(Set.of("THERE_IS_NO_SUCH_REGISTER")); + } + + @Test + public void testReadLiveRegister() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + mb.createTestThreadRegisterBanks(); + mb.testProcess1.regs.addRegistersFromLanguage(getToyBE64Language(), r -> true); + mb.testBank1.writeRegister("r0", mb.arr(1, 2, 3, 4, 5, 6, 7, 8)); + waitOn(mb.testModel.flushEvents()); + TraceRecorder recorder = record(mb.testProcess1); + waitRecorder(recorder); + useTrace(recorder.getTrace()); + traceManager.activateThread(recorder.getTraceThread(mb.testThread1)); + waitForSwing(); + + RegisterValue rv = flat.readRegister("r0"); + assertEquals(BigInteger.valueOf(0x0102030405060708L), rv.getUnsignedValue()); + } + + @Test + public void testReadLiveRegisters() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + mb.createTestThreadRegisterBanks(); + mb.testProcess1.regs.addRegistersFromLanguage(getToyBE64Language(), r -> true); + mb.testBank1.writeRegister("r0", mb.arr(1, 2, 3, 4, 5, 6, 7, 8)); + mb.testBank1.writeRegister("r1", mb.arr(8, 7, 6, 5, 4, 3, 2, 1)); + waitOn(mb.testModel.flushEvents()); + TraceRecorder recorder = record(mb.testProcess1); + waitRecorder(recorder); + useTrace(recorder.getTrace()); + traceManager.activateThread(recorder.getTraceThread(mb.testThread1)); + waitForSwing(); + + Register r0 = tb.language.getRegister("r0"); + Register r1 = tb.language.getRegister("r1"); + assertEquals(List.of( + new RegisterValue(r0, BigInteger.valueOf(0x0102030405060708L)), + new RegisterValue(r1, BigInteger.valueOf(0x0807060504030201L))), + flat.readRegistersNamed(List.of("r0", "r1"))); + } + + @Test + public void testWriteMemoryGivenContext() throws Throwable { + createTraceWithBinText(); + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + + assertTrue(flat.writeMemory(tb.trace, 0, tb.addr(0x00400123), tb.arr(3, 2, 1))); + ByteBuffer buf = ByteBuffer.allocate(3); + assertEquals(3, tb.trace.getMemoryManager().getBytes(0, tb.addr(0x00400123), buf)); + assertArrayEquals(tb.arr(3, 2, 1), buf.array()); + } + + @Test + public void testWriteMemoryCurrentContext() throws Throwable { + createTraceWithBinText(); + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + + assertTrue(flat.writeMemory(tb.addr(0x00400123), tb.arr(3, 2, 1))); + ByteBuffer buf = ByteBuffer.allocate(3); + assertEquals(3, tb.trace.getMemoryManager().getBytes(0, tb.addr(0x00400123), buf)); + assertArrayEquals(tb.arr(3, 2, 1), buf.array()); + } + + @Test + public void testWriteRegisterGivenContext() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + traceManager.activateThread(thread); + waitForSwing(); + + assertTrue(flat.writeRegister(thread, 0, 0, "r0", BigInteger.valueOf(0x0102030405060708L))); + DBTraceMemoryRegisterSpace regs = + tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, false); + assertNotNull(regs); + Register r0 = tb.language.getRegister("r0"); + assertEquals(new RegisterValue(r0, BigInteger.valueOf(0x0102030405060708L)), + regs.getValue(0, r0)); + } + + @Test + public void testWriteRegisterCurrentContext() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + traceManager.activateThread(thread); + waitForSwing(); + + assertTrue(flat.writeRegister("r0", BigInteger.valueOf(0x0102030405060708L))); + DBTraceMemoryRegisterSpace regs = + tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, false); + assertNotNull(regs); + Register r0 = tb.language.getRegister("r0"); + assertEquals(new RegisterValue(r0, BigInteger.valueOf(0x0102030405060708L)), + regs.getValue(0, r0)); + } + + @Test + public void testGetLaunchOffers() throws Throwable { + createProgram(); + programManager.openProgram(program); + waitForSwing(); + + DebuggerProgramLaunchOffer offer = Unique.assertOne(flat.getLaunchOffers()); + assertEquals(TestDebuggerProgramLaunchOffer.class, offer.getClass()); + } + + @Test + public void testLaunchCustomCommandLine() throws Throwable { + createProgram(); + programManager.openProgram(program); + waitForSwing(); + + var model = new TestDebuggerObjectModel() { + Map observedParams; + + @Override + protected TestTargetSession newTestTargetSession(String rootHint) { + return new TestTargetSession(this, "Session", ROOT_SCHEMA) { + @Override + public CompletableFuture launch(Map params) { + observedParams = params; + throw new CancellationException(); + } + }; + } + }; + DebuggerProgramLaunchOffer offer = new TestOffer(program, model); + + LaunchResult result = flat.launch(offer, "custom command line", monitor); + + assertEquals("custom command line", + model.observedParams.get(TargetCmdLineLauncher.CMDLINE_ARGS_NAME)); + assertNotNull(result.model()); + assertNull(result.target()); + assertEquals(CancellationException.class, result.exception().getClass()); + } + + protected TraceRecorder record(TargetObject target) + throws LanguageNotFoundException, CompilerSpecNotFoundException, IOException { + return modelService.recordTargetAndActivateTrace(target, + new TestDebuggerTargetTraceMapper(target)); + } + + @Test + public void testGetTarget() throws Exception { + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = record(mb.testProcess1); + + assertEquals(mb.testProcess1, flat.getTarget(recorder.getTrace())); + } + + @Test + public void testGetTargetThread() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = record(mb.testProcess1); + waitRecorder(recorder); + + Trace trace = recorder.getTrace(); + TraceThread thread = + trace.getThreadManager() + .getLiveThreadByPath(recorder.getSnap(), "Processes[1].Threads[1]"); + assertNotNull(thread); + assertEquals(mb.testThread1, flat.getTargetThread(thread)); + } + + @Test + public void testGetTargetFocus() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = record(mb.testProcess1); + waitRecorder(recorder); + + mb.testModel.requestFocus(mb.testThread2); + waitRecorder(recorder); + + assertEquals(mb.testThread2, flat.getTargetFocus(recorder.getTrace())); + } + + protected void runTestStep(Function step, TargetStepKind kind) + throws Throwable { + var model = new TestDebuggerObjectModel() { + TestTargetThread observedThread; + TargetStepKind observedKind; + + @Override + protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, + int tid) { + return new TestTargetThread(container, tid) { + @Override + public CompletableFuture step(TargetStepKind kind) { + observedThread = this; + observedKind = kind; + return super.step(kind); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = record(mb.testProcess1); + recorder.requestFocus(mb.testThread2); + waitRecorder(recorder); + + assertTrue(step.apply(flat)); + waitRecorder(recorder); + assertEquals(mb.testThread2, model.observedThread); + assertEquals(kind, model.observedKind); + } + + @Test + public void testStepGivenThread() throws Throwable { + runTestStep(flat -> flat.step(flat.getCurrentThread(), TargetStepKind.INTO), + TargetStepKind.INTO); + } + + @Test + public void testStepInto() throws Throwable { + runTestStep(FlatDebuggerAPI::stepInto, TargetStepKind.INTO); + } + + @Test + public void testStepOver() throws Throwable { + runTestStep(FlatDebuggerAPI::stepOver, TargetStepKind.OVER); + } + + @Test + public void testStepOut() throws Throwable { + runTestStep(FlatDebuggerAPI::stepOut, TargetStepKind.FINISH); + } + + protected void runTestResume(Function resume) throws Throwable { + var model = new TestDebuggerObjectModel() { + TestTargetThread observedThread; + + @Override + protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, + int tid) { + return new TestTargetThread(container, tid) { + @Override + public CompletableFuture resume() { + observedThread = this; + return super.resume(); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = record(mb.testProcess1); + recorder.requestFocus(mb.testThread2); + waitRecorder(recorder); + + assertTrue(resume.apply(flat)); + waitRecorder(recorder); + assertEquals(mb.testThread2, model.observedThread); + } + + @Test + public void testResumeGivenThread() throws Throwable { + runTestResume(flat -> flat.resume(flat.getCurrentThread())); + } + + @Test + public void testResumeGivenTrace() throws Throwable { + runTestResume(flat -> flat.resume(flat.getCurrentTrace())); + } + + @Test + public void testResume() throws Throwable { + runTestResume(FlatDebuggerAPI::resume); + } + + protected void runTestInterrupt(Function interrupt) throws Throwable { + var model = new TestDebuggerObjectModel() { + TestTargetThread observedThread; + + @Override + protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, + int tid) { + return new TestTargetThread(container, tid) { + @Override + public CompletableFuture interrupt() { + observedThread = this; + return super.resume(); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = record(mb.testProcess1); + recorder.requestFocus(mb.testThread2); + waitRecorder(recorder); + + assertTrue(interrupt.apply(flat)); + waitRecorder(recorder); + assertEquals(mb.testThread2, model.observedThread); + } + + @Test + public void testInterruptGivenThread() throws Throwable { + runTestInterrupt(flat -> flat.interrupt(flat.getCurrentThread())); + } + + @Test + public void testInterruptGivenTrace() throws Throwable { + runTestInterrupt(flat -> flat.interrupt(flat.getCurrentTrace())); + } + + @Test + public void testInterrupt() throws Throwable { + runTestInterrupt(FlatDebuggerAPI::interrupt); + } + + protected void runTestKill(Function kill) throws Throwable { + var model = new TestDebuggerObjectModel() { + TestTargetThread observedThread; + + @Override + protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, + int tid) { + return new TestTargetThread(container, tid) { + @Override + public CompletableFuture kill() { + observedThread = this; + return super.resume(); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = record(mb.testProcess1); + recorder.requestFocus(mb.testThread2); + waitRecorder(recorder); + + assertTrue(kill.apply(flat)); + waitRecorder(recorder); + assertEquals(mb.testThread2, model.observedThread); + } + + @Test + public void testKillGivenThread() throws Throwable { + runTestKill(flat -> flat.kill(flat.getCurrentThread())); + } + + @Test + public void testKillGivenTrace() throws Throwable { + runTestKill(flat -> flat.kill(flat.getCurrentTrace())); + } + + @Test + public void testKill() throws Throwable { + runTestKill(FlatDebuggerAPI::kill); + } + + protected void runTestExecuteCapture(BiFunction executeCapture) + throws Throwable { + // NOTE: Can't use TestTargetInterpreter.queueExecute stuff, since flat API waits + var model = new TestDebuggerObjectModel() { + @Override + protected TestTargetInterpreter newTestTargetInterpreter(TestTargetSession session) { + return new TestTargetInterpreter(session) { + @Override + public CompletableFuture executeCapture(String cmd) { + return CompletableFuture.completedFuture("Response to " + cmd); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = record(mb.testProcess1); + recorder.requestFocus(mb.testThread2); + waitRecorder(recorder); + + assertEquals("Response to cmd", executeCapture.apply(flat, "cmd")); + } + + @Test + public void testExecuteCaptureGivenTrace() throws Throwable { + runTestExecuteCapture((flat, cmd) -> flat.executeCapture(flat.getCurrentTrace(), cmd)); + } + + @Test + public void testExecuteCapture() throws Throwable { + runTestExecuteCapture(FlatDebuggerAPI::executeCapture); + } + + protected void createProgramWithText() throws Throwable { + createProgram(); + programManager.openProgram(program); + waitForSwing(); + + try (UndoableTransaction tid = UndoableTransaction.start(program, "Add block", true)) { + program.getMemory() + .createInitializedBlock( + ".text", addr(program, 0x00400000), 1024, (byte) 0, monitor, false); + } + } + + protected void createProgramWithBreakpoint() throws Throwable { + createProgramWithText(); + + CompletableFuture changesSettled = breakpointService.changesSettled(); + waitOn(breakpointService.placeBreakpointAt(program, addr(program, 0x00400000), 1, + Set.of(TraceBreakpointKind.SW_EXECUTE), "name")); + waitForSwing(); + waitOn(changesSettled); + } + + @Test + public void testGetAllBreakpoints() throws Throwable { + createProgramWithBreakpoint(); + + assertEquals(1, flat.getAllBreakpoints().size()); + } + + @Test + public void testGetBreakpointsAt() throws Throwable { + createProgramWithBreakpoint(); + + assertEquals(1, flat.getBreakpointsAt(flat.staticLocation("00400000")).size()); + assertEquals(0, flat.getBreakpointsAt(flat.staticLocation("00400001")).size()); + } + + @Test + public void testGetBreakpointsNamed() throws Throwable { + createProgramWithBreakpoint(); + + assertEquals(1, flat.getBreakpointsNamed("name").size()); + assertEquals(0, flat.getBreakpointsNamed("miss").size()); + } + + @Test + public void testBreakpointsToggle() throws Throwable { + createProgramWithBreakpoint(); + LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); + + assertEquals(State.INEFFECTIVE_ENABLED, lb.computeState()); + assertEquals(Set.of(lb), flat.breakpointsToggle(flat.staticLocation("00400000"))); + assertEquals(State.INEFFECTIVE_DISABLED, lb.computeState()); + } + + @Test + public void testBreakpointSetSoftwareExecute() throws Throwable { + createProgramWithText(); + + LogicalBreakpoint lb = Unique.assertOne( + flat.breakpointSetSoftwareExecute(flat.staticLocation("00400000"), "name")); + assertEquals(addr(program, 0x00400000), lb.getAddress()); + assertEquals(TraceBreakpointKindSet.SW_EXECUTE, lb.getKinds()); + assertEquals(1, lb.getLength()); + } + + @Test + public void testBreakpointSetHardwareExecute() throws Throwable { + createProgramWithText(); + + LogicalBreakpoint lb = Unique.assertOne( + flat.breakpointSetHardwareExecute(flat.staticLocation("00400000"), "name")); + assertEquals(addr(program, 0x00400000), lb.getAddress()); + assertEquals(TraceBreakpointKindSet.HW_EXECUTE, lb.getKinds()); + assertEquals(1, lb.getLength()); + } + + @Test + public void testBreakpointSetRead() throws Throwable { + createProgramWithText(); + + LogicalBreakpoint lb = Unique.assertOne( + flat.breakpointSetRead(flat.staticLocation("00400000"), 4, "name")); + assertEquals(addr(program, 0x00400000), lb.getAddress()); + assertEquals(TraceBreakpointKindSet.READ, lb.getKinds()); + assertEquals(4, lb.getLength()); + } + + @Test + public void testBreakpointSetWrite() throws Throwable { + createProgramWithText(); + + LogicalBreakpoint lb = Unique.assertOne( + flat.breakpointSetWrite(flat.staticLocation("00400000"), 4, "name")); + assertEquals(addr(program, 0x00400000), lb.getAddress()); + assertEquals(TraceBreakpointKindSet.WRITE, lb.getKinds()); + assertEquals(4, lb.getLength()); + } + + @Test + public void testBreakpointSetAccess() throws Throwable { + createProgramWithText(); + + LogicalBreakpoint lb = Unique.assertOne( + flat.breakpointSetAccess(flat.staticLocation("00400000"), 4, "name")); + assertEquals(addr(program, 0x00400000), lb.getAddress()); + assertEquals(TraceBreakpointKindSet.ACCESS, lb.getKinds()); + assertEquals(4, lb.getLength()); + } + + @Test + public void testBreakpointsEnable() throws Throwable { + createProgramWithBreakpoint(); + LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); + CompletableFuture changesSettled = breakpointService.changesSettled(); + waitOn(lb.disable()); + waitForSwing(); + waitOn(changesSettled); + + assertEquals(State.INEFFECTIVE_DISABLED, lb.computeState()); + assertEquals(Set.of(lb), flat.breakpointsEnable(flat.staticLocation("00400000"))); + assertEquals(State.INEFFECTIVE_ENABLED, lb.computeState()); + } + + @Test + public void testBreakpointsDisable() throws Throwable { + createProgramWithBreakpoint(); + LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); + + assertEquals(State.INEFFECTIVE_ENABLED, lb.computeState()); + assertEquals(Set.of(lb), flat.breakpointsDisable(flat.staticLocation("00400000"))); + assertEquals(State.INEFFECTIVE_DISABLED, lb.computeState()); + } + + @Test + public void testBreakpointsClear() throws Throwable { + createProgramWithBreakpoint(); + LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); + + assertTrue(flat.breakpointsClear(flat.staticLocation("00400000"))); + assertTrue(lb.isEmpty()); + assertEquals(0, breakpointService.getAllBreakpoints().size()); + } + + @Test + public void testGetModelValue() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + record(mb.testProcess1); + + assertEquals(mb.testThread2, flat.getModelValue("Processes[1].Threads[2]")); + } + + @Test + public void testRefreshObjectChildren() throws Throwable { + var model = new TestDebuggerObjectModel() { + Set observed = new HashSet<>(); + + @Override + protected TestTargetProcess newTestTargetProcess(TestTargetProcessContainer container, + int pid, AddressSpace space) { + return new TestTargetProcess(container, pid, space) { + @Override + public CompletableFuture resync(boolean refreshAttributes, + boolean refreshElements) { + observed.add(this); + return super.resync(refreshAttributes, refreshElements); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + + flat.refreshObjectChildren(mb.testProcess1); + assertEquals(Set.of(mb.testProcess1), model.observed); + } + + @Test + public void testRefreshSubtree() throws Throwable { + var model = new TestDebuggerObjectModel() { + Set observed = new HashSet<>(); + + @Override + protected TestTargetProcess newTestTargetProcess(TestTargetProcessContainer container, + int pid, AddressSpace space) { + return new TestTargetProcess(container, pid, space) { + @Override + public CompletableFuture resync(boolean refreshAttributes, + boolean refreshElements) { + observed.add(this); + return super.resync(refreshAttributes, refreshElements); + } + }; + } + + @Override + protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, + int tid) { + return new TestTargetThread(container, tid) { + @Override + public CompletableFuture resync(boolean refreshAttributes, + boolean refreshElements) { + observed.add(this); + return super.resync(refreshAttributes, refreshElements); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + + flat.refreshSubtree(mb.testModel.session); + assertEquals(Set.of(mb.testProcess1, mb.testProcess3, mb.testThread1, mb.testThread2, + mb.testThread3, mb.testThread4), model.observed); + } + + @Test + public void testFlushAsyncPipelines() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = record(mb.testProcess1); + + // Ensure it works whether or not there are pending events + for (int i = 0; i < 10; i++) { + flat.flushAsyncPipelines(recorder.getTrace()); + } + } +} diff --git a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncDebouncer.java b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncDebouncer.java index a10ef9ad1b..2a9e66da7a 100644 --- a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncDebouncer.java +++ b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncDebouncer.java @@ -47,7 +47,7 @@ public class AsyncDebouncer { * Construct a new debouncer * * @param timer the timer to use for delay - * @param windowMillis the timing window of changes to elide + * @param windowMillis the timing window of changes to ignore */ public AsyncDebouncer(AsyncTimer timer, long windowMillis) { this.timer = timer; @@ -115,7 +115,7 @@ public class AsyncDebouncer { *

* The returned future completes after all registered listeners have been invoked. * - * @return a future which completes with the value of the next settled event. + * @return a future which completes with the value of the next settled event */ public synchronized CompletableFuture settled() { if (settledPromise == null) { @@ -123,4 +123,22 @@ public class AsyncDebouncer { } return settledPromise; } + + /** + * Wait for the debouncer to be stable + * + *

+ * If the debouncer has not received a contact event within the event window, it's considered + * stable, and this returns a completed future with the value of the last received contact + * event. Otherwise, the returned future completes on the next settled event, as in + * {@link #settled()}. + * + * @return a future which completes, perhaps immediately, when the debouncer is stable + */ + public synchronized CompletableFuture stable() { + if (alarm == null) { + return CompletableFuture.completedFuture(lastContact); + } + return settled(); + } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/AnnotatedDebuggerAttributeListener.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/AnnotatedDebuggerAttributeListener.java index 7bbeb5facc..3155c1e8f6 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/AnnotatedDebuggerAttributeListener.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/AnnotatedDebuggerAttributeListener.java @@ -25,12 +25,24 @@ import java.util.*; import ghidra.dbg.target.TargetObject; import ghidra.util.Msg; +/** + * A model listener that permits {@link AttributeCallback} annotations for convenient callbacks when + * the named attribute changes + */ public abstract class AnnotatedDebuggerAttributeListener implements DebuggerModelListener { private static final String ATTR_METHODS = "@" + AttributeCallback.class.getSimpleName() + "-annotated methods"; private static final String PARAMS_ERR = ATTR_METHODS + " must accept 2 parameters: (TargetObject, T)"; + /** + * Annotation for a method receiving an attribute change callback + * + *

+ * The annotated method must accept parameters {@code (TargetObject, T)}, where {@code T} is the + * type of the attribute. Currently, very little checks are applied during construction. + * Incorrect use will result in errors during callback invocation. + */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) protected @interface AttributeCallback { diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/DebuggerCallbackReorderer.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/DebuggerCallbackReorderer.java index 0904e52a55..184f666319 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/DebuggerCallbackReorderer.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/DebuggerCallbackReorderer.java @@ -374,4 +374,8 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener { rec.cancel(); } } + + public CompletableFuture flushEvents() { + return lastEvent.thenApply(v -> v); + } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerModelBuilder.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerModelBuilder.java index 340ed0a666..af8f8157c7 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerModelBuilder.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerModelBuilder.java @@ -40,12 +40,16 @@ public class TestDebuggerModelBuilder { public TestTargetRegister testRegisterPC; public TestTargetRegister testRegisterSP; + protected TestDebuggerObjectModel newModel(String typeHint) { + return new TestDebuggerObjectModel(typeHint); + } + public void createTestModel() { createTestModel("Session"); } public void createTestModel(String typeHint) { - testModel = new TestDebuggerObjectModel(typeHint); + testModel = newModel(typeHint); } public Address addr(long offset) { diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java index 8fc5cab2b4..deeaaf2c05 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java @@ -16,15 +16,19 @@ package ghidra.dbg.model; import java.io.IOException; +import java.util.Set; import java.util.concurrent.*; import org.jdom.JDOMException; import ghidra.dbg.DebuggerModelListener; +import ghidra.dbg.attributes.TargetDataType; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.XmlSchemaContext; -import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.Register; // TODO: Refactor with other Fake and Test model stuff. public class TestDebuggerObjectModel extends EmptyDebuggerObjectModel { @@ -60,13 +64,145 @@ public class TestDebuggerObjectModel extends EmptyDebuggerObjectModel { this("Session"); } - public Executor getClientExecutor() { - return clientExecutor; + public TestDebuggerObjectModel(String rootHint) { + this.session = newTestTargetSession(rootHint); + addModelRoot(session); } - public TestDebuggerObjectModel(String rootHint) { - this.session = new TestTargetSession(this, rootHint, ROOT_SCHEMA); - addModelRoot(session); + protected TestTargetSession newTestTargetSession(String rootHint) { + return new TestTargetSession(this, rootHint, ROOT_SCHEMA); + } + + protected TestTargetEnvironment newTestTargetEnvironment(TestTargetSession session) { + return new TestTargetEnvironment(session); + } + + protected TestTargetProcessContainer newTestTargetProcessContainer(TestTargetSession session) { + return new TestTargetProcessContainer(session); + } + + protected TestTargetProcess newTestTargetProcess(TestTargetProcessContainer container, int pid, + AddressSpace space) { + return new TestTargetProcess(container, pid, space); + } + + protected TestTargetBreakpointContainer newTestTargetBreakpointContainer( + TestTargetProcess process) { + return new TestTargetBreakpointContainer(process); + } + + protected TestTargetBreakpoint newTestTargetBreakpoint(TestTargetBreakpointContainer container, + int num, Address address, int length, Set kinds) { + return new TestTargetBreakpoint(container, num, address, length, kinds); + } + + protected TestTargetMemory newTestTargetMemory(TestTargetProcess process, AddressSpace space) { + return new TestTargetMemory(process, space); + } + + protected TestTargetMemoryRegion newTestTargetMemoryRegion(TestTargetMemory memory, String name, + AddressRange range, String flags) { + return new TestTargetMemoryRegion(memory, name, range, flags); + } + + protected TestTargetModuleContainer newTestTargetModuleContainer(TestTargetProcess process) { + return new TestTargetModuleContainer(process); + } + + protected TestTargetModule newTestTargetModule(TestTargetModuleContainer container, String name, + AddressRange range) { + return new TestTargetModule(container, name, range); + } + + protected TestTargetSectionContainer newTestTargetSectionContainer(TestTargetModule module) { + return new TestTargetSectionContainer(module); + } + + protected TestTargetSection newTestTargetSection(TestTargetSectionContainer container, + String name, AddressRange range) { + return new TestTargetSection(container, name, range); + } + + protected TestTargetSymbolNamespace newTestTargetSymbolNamespace(TestTargetModule module) { + return new TestTargetSymbolNamespace(module); + } + + protected TestTargetSymbol newTestTargetSymbol(TestTargetSymbolNamespace namespace, String name, + Address address, long size, TargetDataType dataType) { + return new TestTargetSymbol(namespace, name, address, size, dataType); + } + + protected TestTargetDataTypeNamespace newTestTargetDataTypeNamespace(TestTargetModule module) { + return new TestTargetDataTypeNamespace(module); + } + + protected TestTargetTypedefDataType newTestTargetTypedefDataType( + TestTargetDataTypeNamespace namespace, String name, TargetDataType defDataType) { + return new TestTargetTypedefDataType(namespace, name, defDataType); + } + + protected TestTargetTypedefDef newTestTargetTypedefDef(TestTargetTypedefDataType typedef, + TargetDataType dataType) { + return new TestTargetTypedefDef(typedef, dataType); + } + + protected TestTargetRegisterContainer newTestTargetRegisterContainer( + TestTargetProcess process) { + return new TestTargetRegisterContainer(process); + } + + protected TestTargetRegister newTestTargetRegister(TestTargetRegisterContainer container, + Register register) { + return TestTargetRegister.fromLanguageRegister(container, register); + } + + protected TestTargetThreadContainer newTestTargetThreadContainer(TestTargetProcess process) { + return new TestTargetThreadContainer(process); + } + + protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, int tid) { + return new TestTargetThread(container, tid); + } + + protected TestTargetRegisterBankInThread newTestTargetRegisterBankInThread( + TestTargetThread thread) { + return new TestTargetRegisterBankInThread(thread); + } + + protected TestTargetStack newTestTargetStack(TestTargetThread thread) { + return new TestTargetStack(thread); + } + + protected TestTargetStackFrameNoRegisterBank newTestTargetStackFrameNoRegisterBank( + TestTargetStack stack, int level, Address pc) { + return new TestTargetStackFrameNoRegisterBank(stack, level, pc); + } + + protected TestTargetStackFrameHasRegisterBank newTestTargetStackFrameHasRegisterBank( + TestTargetStack stack, int level, Address pc) { + return new TestTargetStackFrameHasRegisterBank(stack, level, pc); + } + + protected TestTargetRegisterBankInFrame newTestTargetRegisterBankInFrame( + TestTargetStackFrameHasRegisterBank frame) { + return new TestTargetRegisterBankInFrame(frame); + } + + protected TestTargetStackFrameIsRegisterBank newTestTargetStackFrameIsRegisterBank( + TestTargetStack stack, int level, Address pc) { + return new TestTargetStackFrameIsRegisterBank(stack, level, pc); + } + + protected TestTargetInterpreter newTestTargetInterpreter(TestTargetSession session) { + return new TestTargetInterpreter(session); + } + + protected TestMimickJavaLauncher newTestMimickJavaLauncher(TestTargetSession session) { + return new TestMimickJavaLauncher(session); + } + + public Executor getClientExecutor() { + return clientExecutor; } @Override diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpointContainer.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpointContainer.java index f7f01f60cf..6234660e7c 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpointContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpointContainer.java @@ -56,8 +56,9 @@ public class TestTargetBreakpointContainer @Override public CompletableFuture placeBreakpoint(AddressRange range, Set kinds) { - TestTargetBreakpoint bpt = new TestTargetBreakpoint(this, counter.getAndIncrement(), - range.getMinAddress(), (int) range.getLength(), kinds); + TestTargetBreakpoint bpt = + getModel().newTestTargetBreakpoint(this, counter.getAndIncrement(), + range.getMinAddress(), (int) range.getLength(), kinds); changeElements(List.of(), List.of(bpt), "Breakpoint Added"); return getModel().future(null); } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetDataTypeNamespace.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetDataTypeNamespace.java index 941162813e..a42394d0ff 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetDataTypeNamespace.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetDataTypeNamespace.java @@ -30,7 +30,8 @@ public class TestTargetDataTypeNamespace public TestTargetTypedefDataType addTypedefDataType(String name, TargetDataType defDataType) { - TestTargetTypedefDataType dataType = new TestTargetTypedefDataType(this, name, defDataType); + TestTargetTypedefDataType dataType = + getModel().newTestTargetTypedefDataType(this, name, defDataType); changeElements(List.of(), List.of(dataType), "Added typedef " + name); return dataType; } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetMemory.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetMemory.java index 0febf2201e..a1934519f2 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetMemory.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetMemory.java @@ -73,7 +73,8 @@ public class TestTargetMemory } public TestTargetMemoryRegion addRegion(String name, AddressRange range, String flags) { - TestTargetMemoryRegion region = new TestTargetMemoryRegion(this, name, range, flags); + TestTargetMemoryRegion region = + getModel().newTestTargetMemoryRegion(this, name, range, flags); changeElements(List.of(), List.of(region), "Add test region: " + range); return region; } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetModule.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetModule.java index 28e1e976c0..ce777124ce 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetModule.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetModule.java @@ -32,9 +32,9 @@ public class TestTargetModule public TestTargetModule(TestTargetModuleContainer parent, String name, AddressRange range) { super(parent, PathUtils.makeKey(name), "Module"); - sections = new TestTargetSectionContainer(this); - symbols = new TestTargetSymbolNamespace(this); - types = new TestTargetDataTypeNamespace(this); + sections = getModel().newTestTargetSectionContainer(this); + symbols = getModel().newTestTargetSymbolNamespace(this); + types = getModel().newTestTargetDataTypeNamespace(this); changeAttributes(List.of(), Map.of( RANGE_ATTRIBUTE_NAME, range, diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetModuleContainer.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetModuleContainer.java index 3148cfc356..97f9d96996 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetModuleContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetModuleContainer.java @@ -29,7 +29,7 @@ public class TestTargetModuleContainer } public TestTargetModule addModule(String name, AddressRange range) { - TestTargetModule module = new TestTargetModule(this, name, range); + TestTargetModule module = getModel().newTestTargetModule(this, name, range); changeElements(List.of(), List.of(module), "Add test module: " + name); return module; } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcess.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcess.java index 953375b9f4..1a5d58f943 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcess.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcess.java @@ -35,11 +35,11 @@ public class TestTargetProcess extends public TestTargetProcess(DefaultTestTargetObject parent, int pid, AddressSpace space) { super(parent, PathUtils.makeKey(PathUtils.makeIndex(pid)), "Process"); - breaks = new TestTargetBreakpointContainer(this); - memory = new TestTargetMemory(this, space); - modules = new TestTargetModuleContainer(this); - regs = new TestTargetRegisterContainer(this); - threads = new TestTargetThreadContainer(this); + breaks = getModel().newTestTargetBreakpointContainer(this); + memory = getModel().newTestTargetMemory(this, space); + modules = getModel().newTestTargetModuleContainer(this); + regs = getModel().newTestTargetRegisterContainer(this); + threads = getModel().newTestTargetThreadContainer(this); changeAttributes(List.of(), List.of( breaks, diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcessContainer.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcessContainer.java index 2ef0ed1ec0..3521be23b4 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcessContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetProcessContainer.java @@ -27,7 +27,7 @@ public class TestTargetProcessContainer } public TestTargetProcess addProcess(int pid, AddressSpace space) { - TestTargetProcess proc = new TestTargetProcess(this, pid, space); + TestTargetProcess proc = getModel().newTestTargetProcess(this, pid, space); changeElements(List.of(), List.of(proc), Map.of(), "Test Process Added"); return proc; } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetRegisterContainer.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetRegisterContainer.java index 12c89729e8..d0a1b28431 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetRegisterContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetRegisterContainer.java @@ -41,14 +41,14 @@ public class TestTargetRegisterContainer if (!predicate.test(register)) { continue; } - add.add(TestTargetRegister.fromLanguageRegister(this, register)); + add.add(getModel().newTestTargetRegister(this, register)); } changeElements(List.of(), add, "Added registers from Ghidra language: " + language); return add; } public TestTargetRegister addRegister(Register register) { - TestTargetRegister tr = TestTargetRegister.fromLanguageRegister(this, register); + TestTargetRegister tr = getModel().newTestTargetRegister(this, register); changeElements(List.of(), List.of(tr), "Added " + register + " from Ghidra language"); return tr; } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSectionContainer.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSectionContainer.java index df6c2c04eb..0b33a7c9c3 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSectionContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSectionContainer.java @@ -29,7 +29,7 @@ public class TestTargetSectionContainer } public TestTargetSection addSection(String name, AddressRange range) { - TestTargetSection section = new TestTargetSection(this, name, range); + TestTargetSection section = getModel().newTestTargetSection(this, name, range); changeElements(List.of(), List.of(section), "Add test section: " + name); return section; } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSession.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSession.java index cfb72ab3ed..9fdded43dd 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSession.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSession.java @@ -42,10 +42,10 @@ public class TestTargetSession extends DefaultTargetModelRoot public TestTargetSession(TestDebuggerObjectModel model, String rootHint, TargetObjectSchema schema) { super(model, rootHint, schema); - environment = new TestTargetEnvironment(this); - processes = new TestTargetProcessContainer(this); - interpreter = new TestTargetInterpreter(this); - mimickJavaLauncher = new TestMimickJavaLauncher(this); + environment = model.newTestTargetEnvironment(this); + processes = model.newTestTargetProcessContainer(this); + interpreter = model.newTestTargetInterpreter(this); + mimickJavaLauncher = model.newTestMimickJavaLauncher(this); changeAttributes(List.of(), List.of(environment, processes, interpreter, mimickJavaLauncher), Map.of(), diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetStack.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetStack.java index 94b36e26fc..6618097990 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetStack.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetStack.java @@ -39,7 +39,8 @@ public class TestTargetStack extends DefaultTestTargetObject - implements TargetThread, TargetExecutionStateful { + implements TargetThread, TargetExecutionStateful, TargetSteppable, TargetResumable, + TargetInterruptible, TargetKillable { + + public static final TargetStepKindSet SUPPORTED_KINDS = + TargetStepKindSet.of(TargetStepKind.values()); + public TestTargetThread(TestTargetThreadContainer parent, int tid) { super(parent, PathUtils.makeKey(PathUtils.makeIndex(tid)), "Thread"); changeAttributes(List.of(), List.of(), Map.of( - STATE_ATTRIBUTE_NAME, TargetExecutionState.STOPPED // - ), "Initialized"); + STATE_ATTRIBUTE_NAME, TargetExecutionState.STOPPED, + SUPPORTED_STEP_KINDS_ATTRIBUTE_NAME, SUPPORTED_KINDS), + "Initialized"); } /** @@ -39,7 +45,7 @@ public class TestTargetThread * @return the created register bank */ public TestTargetRegisterBankInThread addRegisterBank() { - TestTargetRegisterBankInThread regs = new TestTargetRegisterBankInThread(this); + TestTargetRegisterBankInThread regs = getModel().newTestTargetRegisterBankInThread(this); changeAttributes(List.of(), List.of( regs), Map.of(), "Add Test Register Bank"); @@ -47,7 +53,7 @@ public class TestTargetThread } public TestTargetStack addStack() { - TestTargetStack stack = new TestTargetStack(this); + TestTargetStack stack = getModel().newTestTargetStack(this); changeAttributes(List.of(), List.of( stack), Map.of(), "Add Test Stack"); @@ -55,8 +61,28 @@ public class TestTargetThread } public void setState(TargetExecutionState state) { - Delta delta = changeAttributes(List.of(), List.of(), Map.of( - STATE_ATTRIBUTE_NAME, state // - ), "Changed state"); + changeAttributes(List.of(), List.of(), Map.of( + STATE_ATTRIBUTE_NAME, state), + "Changed state"); + } + + @Override + public CompletableFuture step(TargetStepKind kind) { + return AsyncUtils.NIL; + } + + @Override + public CompletableFuture resume() { + return AsyncUtils.NIL; + } + + @Override + public CompletableFuture interrupt() { + return AsyncUtils.NIL; + } + + @Override + public CompletableFuture kill() { + return AsyncUtils.NIL; } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetThreadContainer.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetThreadContainer.java index c450f572ba..bfb1e93e54 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetThreadContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetThreadContainer.java @@ -24,12 +24,13 @@ import ghidra.dbg.target.TargetObject; public class TestTargetThreadContainer extends DefaultTestTargetObject { + public TestTargetThreadContainer(TestTargetProcess parent) { super(parent, "Threads", "Threads"); } public TestTargetThread addThread(int tid) { - TestTargetThread thread = new TestTargetThread(this, tid); + TestTargetThread thread = getModel().newTestTargetThread(this, tid); changeElements(List.of(), List.of(thread), Map.of(), "Test Thread Added"); return thread; } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetTypedefDataType.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetTypedefDataType.java index de38fd472d..dd2694c6f6 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetTypedefDataType.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetTypedefDataType.java @@ -21,9 +21,11 @@ import ghidra.dbg.attributes.TargetDataType; public class TestTargetTypedefDataType extends TestTargetNamedDataType { + public TestTargetTypedefDataType(TestTargetDataTypeNamespace parent, String name, TargetDataType dataType) { super(parent, name, NamedDataTypeKind.TYPEDEF, "TypedefType"); - changeElements(List.of(), List.of(new TestTargetTypedefDef(this, dataType)), "Initialized"); + changeElements(List.of(), List.of(getModel().newTestTargetTypedefDef(this, dataType)), + "Initialized"); } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml b/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml index eef0f4617f..9b44aa0920 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml +++ b/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml @@ -172,7 +172,10 @@ - + + + +

- * TODO: Implement me - * * @param snap the time to search * @param range the address range to search * @param data the values to search for @@ -422,7 +419,7 @@ public interface TraceMemoryOperations { * @param forward true to return the match with the lowest address in {@code range}, false for * the highest address. * @param monitor a monitor for progress reporting and canceling - * @return the address of the match, or {@code null} if not found + * @return the minimum address of the matched bytes, or {@code null} if not found */ Address findBytes(long snap, AddressRange range, ByteBuffer data, ByteBuffer mask, boolean forward, TaskMonitor monitor); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java index e8d9b60386..66bd4c1a82 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java @@ -479,6 +479,19 @@ public class TraceSchedule implements Comparable { return new TraceSchedule(snap, steps.clone(), pTicks); } + /** + * Behaves as in {@link #steppedPcodeForward(TraceThread, int)}, but by appending skips + * + * @param thread the thread to step, or null for the "last thread" + * @param pTickCount the number of p-code skips to take the thread forward + * @return the resulting schedule + */ + public TraceSchedule skippedPcodeForward(TraceThread thread, int pTickCount) { + Sequence pTicks = this.pSteps.clone(); + pTicks.advance(new SkipStep(thread == null ? -1 : thread.getKey(), pTickCount)); + return new TraceSchedule(snap, steps.clone(), pTicks); + } + /** * Returns the equivalent of executing count p-code operations less than this schedule *