From 034730b7857ba9f1bf8cca34a722987509c5873e Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Tue, 18 Oct 2022 09:59:53 -0400 Subject: [PATCH] GP-1595: Add global control actions for Target, Trace, and Emulator --- .../java/agent/gdb/manager/GdbManager.java | 4 +- .../model/impl/GdbModelTargetInferior.java | 35 +- .../gdb/model/impl/GdbModelTargetThread.java | 5 +- Ghidra/Debug/Debugger/certification.manifest | 22 +- .../src/main/help/help/TOC_Source.xml | 4 +- .../DebuggerControlPlugin.html | 202 ++++ .../images/2leftarrow.png | Bin 0 -> 2249 bytes .../images/2rightarrow.png | Bin 0 -> 2239 bytes .../images/disconnect.png | Bin 0 -> 556 bytes .../images/interrupt.png | Bin 0 -> 211 bytes .../DebuggerControlPlugin/images/kill.png | Bin 0 -> 210 bytes .../DebuggerControlPlugin/images/resume.png | Bin 0 -> 396 bytes .../DebuggerControlPlugin/images/skipover.png | Bin 0 -> 497 bytes .../DebuggerControlPlugin/images/stepback.png | Bin 0 -> 428 bytes .../DebuggerControlPlugin/images/stepinto.png | Bin 0 -> 404 bytes .../DebuggerControlPlugin/images/steplast.png | Bin 0 -> 410 bytes .../DebuggerControlPlugin/images/stepout.png | Bin 0 -> 413 bytes .../DebuggerControlPlugin/images/stepover.png | Bin 0 -> 428 bytes .../images/write-disabled.png | Bin 0 -> 733 bytes .../images/write-emulator.png | Bin 0 -> 826 bytes .../images/write-target.png | Bin 0 -> 694 bytes .../images/write-trace.png | Bin 0 -> 865 bytes .../DebuggerListingPlugin.html | 6 +- .../DebuggerMemoryBytesPlugin.html | 13 +- .../DebuggerObjectsPlugin.html | 4 +- .../DebuggerRegistersPlugin.html | 10 +- .../DebuggerStateEditingPlugin.html | 87 -- .../images/write-disabled.png | Bin 397 -> 0 bytes .../images/write-emulator.png | Bin 559 -> 0 bytes .../images/write-target.png | Bin 414 -> 0 bytes .../images/write-trace.png | Bin 699 -> 0 bytes .../DebuggerWatchesPlugin.html | 14 +- .../core/debug/DebuggerCoordinates.java | 2 +- .../core/debug/gui/DebuggerResources.java | 32 +- .../gui/control/DebuggerControlPlugin.java | 1067 +++++++++++++++++ .../editing/DebuggerStateEditingPlugin.java | 142 --- .../DebuggerEmulationServicePlugin.java | 259 +++- .../model/DebuggerModelServicePlugin.java | 1 - .../service/model/DefaultTraceRecorder.java | 11 + .../record/ObjectBasedTraceRecorder.java | 10 + .../DebuggerTraceManagerServicePlugin.java | 85 +- .../services/DebuggerEmulationService.java | 151 ++- .../ghidra/app/services/TraceRecorder.java | 178 ++- .../main/resources/defaultTools/Debugger.tool | 1 + .../AbstractGhidraHeadedDebuggerGUITest.java | 21 +- .../control/DebuggerControlPluginTest.java | 583 +++++++++ ...ggerStateEditingPluginIntegrationTest.java | 240 ---- .../DebuggerEmulationServiceTest.java | 133 ++ .../dbg/target/TargetExecutionStateful.java | 103 +- .../java/ghidra/dbg/target/TargetObject.java | 16 + .../trace/model/target/TraceObject.java | 29 +- .../trace/model/time/schedule/PatchStep.java | 2 +- .../trace/model/time/schedule/Scheduler.java | 151 +++ .../trace/model/time/schedule/Sequence.java | 4 +- .../trace/model/time/schedule/SkipStep.java | 2 +- .../trace/model/time/schedule/Step.java | 8 +- .../trace/model/time/schedule/Stepper.java | 21 +- .../trace/model/time/schedule/TickStep.java | 2 +- .../model/time/schedule/TraceSchedule.java | 26 + .../pcode/emu/AbstractPcodeMachine.java | 50 +- .../ghidra/pcode/emu/DefaultPcodeThread.java | 21 +- .../java/ghidra/pcode/emu/PcodeMachine.java | 107 +- .../pcode/emu/SparseAddressRangeMap.java | 118 ++ .../java/ghidra/pcode/exec/PcodeExecutor.java | 20 + .../java/ghidra/pcode/exec/PcodeFrame.java | 16 + .../pcode/emu/SparseAddressRangeMapTest.java | 80 ++ 66 files changed, 3335 insertions(+), 763 deletions(-) create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/2leftarrow.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/2rightarrow.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/disconnect.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/interrupt.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/kill.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/resume.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/skipover.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepback.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepinto.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/steplast.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepout.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepover.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-disabled.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-emulator.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-target.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-trace.png delete mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/DebuggerStateEditingPlugin.html delete mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-disabled.png delete mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-emulator.png delete mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-target.png delete mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-trace.png create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java delete mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/editing/DebuggerStateEditingPlugin.java create mode 100644 Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPluginTest.java delete mode 100644 Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/editing/DebuggerStateEditingPluginIntegrationTest.java create mode 100644 Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Scheduler.java create mode 100644 Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/SparseAddressRangeMap.java create mode 100644 Ghidra/Debug/ProposedUtils/src/test/java/ghidra/pcode/emu/SparseAddressRangeMapTest.java diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java index 7a6346e203..e6d653e858 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java @@ -48,9 +48,7 @@ public interface GdbManager extends AutoCloseable, GdbConsoleOperations, GdbBrea RETURN("return"), STEP("step"), STEPI("stepi", "step-instruction"), - UNTIL("until"), - /** User-defined */ - EXTENDED("echo extended-step?", "???"),; + UNTIL("until"); public final String mi2; public final String cli; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java index fa6a1a990a..abb658c70b 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java @@ -19,9 +19,8 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import agent.gdb.manager.*; -import agent.gdb.manager.GdbManager.StepCmd; -import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; import agent.gdb.manager.impl.cmd.GdbConsoleExecCommand.CompletesWithRunning; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; import agent.gdb.manager.reason.*; import ghidra.async.AsyncFence; import ghidra.dbg.agent.DefaultTargetObject; @@ -43,7 +42,7 @@ public class GdbModelTargetInferior extends DefaultTargetObject implements TargetProcess, TargetAggregate, TargetExecutionStateful, TargetAttacher, TargetDeletable, TargetDetachable, TargetKillable, TargetLauncher, TargetResumable, - TargetSteppable, GdbModelSelectableObject { + TargetSteppable, TargetInterruptible, GdbModelSelectableObject { public static final String EXIT_CODE_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "exit_code"; @@ -187,39 +186,18 @@ public class GdbModelTargetInferior return impl.gateFuture(inferior.cont()); } - protected StepCmd convertToGdb(TargetStepKind kind) { - switch (kind) { - case FINISH: - return StepCmd.FINISH; - case INTO: - return StepCmd.STEPI; - case LINE: - return StepCmd.STEP; - case OVER: - return StepCmd.NEXTI; - case OVER_LINE: - return StepCmd.NEXT; - case RETURN: - return StepCmd.RETURN; - case UNTIL: - return StepCmd.UNTIL; - case EXTENDED: - return StepCmd.EXTENDED; - default: - throw new AssertionError(); - } - } @Override public CompletableFuture step(TargetStepKind kind) { switch (kind) { case SKIP: + case EXTENDED: throw new UnsupportedOperationException(kind.name()); case ADVANCE: // Why no exec-advance in GDB/MI? // TODO: This doesn't work, since advance requires a parameter return model.gateFuture(inferior.console("advance", CompletesWithRunning.MUST)); default: - return model.gateFuture(inferior.step(convertToGdb(kind))); + return model.gateFuture(inferior.step(GdbModelTargetThread.convertToGdb(kind))); } } @@ -228,6 +206,11 @@ public class GdbModelTargetInferior return model.gateFuture(inferior.kill()); } + @Override + public CompletableFuture interrupt() { + return impl.session.interrupt(); + } + @Override public CompletableFuture attach(TargetAttachable attachable) { GdbModelTargetAttachable mine = impl.assertMine(GdbModelTargetAttachable.class, attachable); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java index 43b9b2d7b4..5d3eda319d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java @@ -187,7 +187,7 @@ public class GdbModelTargetThread } } - protected StepCmd convertToGdb(TargetStepKind kind) { + protected static StepCmd convertToGdb(TargetStepKind kind) { switch (kind) { case FINISH: return StepCmd.FINISH; @@ -203,8 +203,6 @@ public class GdbModelTargetThread return StepCmd.RETURN; case UNTIL: return StepCmd.UNTIL; - case EXTENDED: - return StepCmd.EXTENDED; default: throw new AssertionError(); } @@ -214,6 +212,7 @@ public class GdbModelTargetThread public CompletableFuture step(TargetStepKind kind) { switch (kind) { case SKIP: + case EXTENDED: throw new UnsupportedOperationException(kind.name()); case ADVANCE: // Why no exec-advance in GDB/MI? // TODO: This doesn't work, since advance requires a parameter diff --git a/Ghidra/Debug/Debugger/certification.manifest b/Ghidra/Debug/Debugger/certification.manifest index 723e3acccf..81429ba7c4 100644 --- a/Ghidra/Debug/Debugger/certification.manifest +++ b/Ghidra/Debug/Debugger/certification.manifest @@ -41,6 +41,23 @@ src/main/help/help/topics/DebuggerBreakpointsPlugin/images/breakpoints-enable-al src/main/help/help/topics/DebuggerBreakpointsPlugin/images/breakpoints-make-effective.png||GHIDRA||||END| src/main/help/help/topics/DebuggerConsolePlugin/DebuggerConsolePlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerConsolePlugin/images/DebuggerConsolePlugin.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/2leftarrow.png||Nuvola Icons - LGPL 2.1||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/2rightarrow.png||Nuvola Icons - LGPL 2.1||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/disconnect.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/interrupt.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/kill.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/resume.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/skipover.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/stepback.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/stepinto.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/steplast.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/stepout.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/stepover.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/write-disabled.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/write-emulator.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/write-target.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerControlPlugin/images/write-trace.png||GHIDRA||||END| src/main/help/help/topics/DebuggerCopyActionsPlugin/DebuggerCopyActionsPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerCopyActionsPlugin/images/DebuggerCopyIntoProgramDialog.png||GHIDRA||||END| src/main/help/help/topics/DebuggerDisassemblerPlugin/DebuggerDisassemblerPlugin.html||GHIDRA||||END| @@ -108,11 +125,6 @@ src/main/help/help/topics/DebuggerRegistersPlugin/images/DebuggerRegistersPlugin src/main/help/help/topics/DebuggerRegistersPlugin/images/select-registers.png||GHIDRA||||END| src/main/help/help/topics/DebuggerStackPlugin/DebuggerStackPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerStackPlugin/images/DebuggerStackPlugin.png||GHIDRA||||END| -src/main/help/help/topics/DebuggerStateEditingPlugin/DebuggerStateEditingPlugin.html||GHIDRA||||END| -src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-disabled.png||GHIDRA||||END| -src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-emulator.png||GHIDRA||||END| -src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-target.png||GHIDRA||||END| -src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-trace.png||Tango Icons - Public Domain||||END| src/main/help/help/topics/DebuggerStaticMappingPlugin/DebuggerStaticMappingPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerStaticMappingPlugin/images/DebuggerStaticMappingPlugin.png||GHIDRA||||END| src/main/help/help/topics/DebuggerTargetsPlugin/DebuggerTargetsPlugin.html||GHIDRA||||END| diff --git a/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml b/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml index d40d06104b..a34ed2bfe5 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml @@ -153,9 +153,9 @@ sortgroup="n" target="help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html" /> - + target="help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html" /> + + + + + + Debugger: Editing Machine State + + + + + +

Debugger: Control and Machine State

+ +

This plugin presents actions for controlling targets and modifying machine state. It + provides a drop-down action in the main toolbar for choosing what to control for the current + target: The live target, the integrated emulator, or the recorded trace. Only those control + actions suitable for the selection are displayed. Machine-state edits throughout the UI are + directed accordingly.

+ +

Actions

+ +

The plugin provides several actions, but only certain ones are displayed, depending on the + current mode.

+ +

Edit Mode

+ +

This action changes the mode for the active trace and, if applicable, its associated live + target. It is always displayed, but only available when a trace is active. The possible modes + are:

+ +
    +
  • Control Target w/Edits Disabled - This + presents actions for controlling the live target but rejects all machine-state edits.
  • + +
  • Control Target - The default, this presents + actions for controlling the live target and directs edits to the live target. To accept + edits, the UI must be "live and at the present." If the trace has no associated target, i.e., + it is dead; or if the current view is in the past or includes any steps of emulation, i.e., + it is not at the present; then edits are rejected.
  • + +
  • Control Trace - This presents actions for + navigating trace snapshots. It directs all edits to the trace database. Edits are generally + always accepted, and they are applied directly to the trace.
  • + +
  • Control Emulator - This presents actions for + controlling the integrated emulator. This can be used for interpolating and extrapolating + execution from the current snapshot, without affecting the live target. It directs edits to + the integrated emulator by generating patch steps and appending them to the emulation + schedule. See the Go To Time + action. Essentially, the change is applied in the trace's scratch space, leaving the original + recording in tact. Due to implementation details, a thread must be selected, even if edits + only affect memory. Additionally, the disassembly context register cannot be modified.
  • +
+ +

Target Control Actions

+ +

These actions are visible when the "Control Target" or "Control Target w/Edits Disabled" + mode is selected. They are available only when the current trace has an associated live target. + Commands are directed to the focused object or a suitable substitute.

+ +

Resume

+ +

Allow the current target to resume execution. Other debuggers may call this "continue" or + "go." If successful, the target enters the "running" state until/if it is interrupted or + terminated. This is available when the target is currently stopped.

+ +

Interrupt

+ +

Interrupt the current target's execution. Other debuggers may call this "break," "suspend," + or "stop." If successful, the target enters the "stopped" state. This is available when the + target is currently running.

+ +

Kill

+ +

Kill the current target. Other debuggers may call this "terminate" or "stop." By default, + this will consequently close the current trace. If successful, the target enters the + "terminated" state. This is always available for a live target.

+ +

Disconnect

+ +

Disconnect from the current target's debugger. This usually causes the connected debugger to + terminate and likely kill its targets. By default, this will consequently close the trace for + all affected targets. This is always available for a live target.

+ +

Step Into

+ +

Step the current target to the next instruction. This is available when the target is + currently stopped. If successful the target may briefly enter the "running" state.

+ +

Step Over

+ +

Step the current target to the next instruction in the current subroutine. This is available + when the target is currently stopped. If successful the target may briefly enter the "running" + state.

+ +

Finish

+ +

Allow the current target to finish the current subroutine, pausing after. This is available + when the target is currently stopped. If successful the target may briefly enter the "running" + state.

+ +

Step Repeat Last / + Extended

+ +

Perform a target-defined step, often the last (possibly custom or extended) step. This is + available when the target is currently stopped. If successful the target may briefly enter the + "running" state.

+ +

Trace Navigation Actions

+ +

These actions are visible when the "Control Trace" mode is selected. They are available when + there is an active trace.

+ +

+ Snapshot Backward

+ +

This navigates the trace backward one snapshot. All windows displaying machine state will + show that recorded in the current snapshot. This is available only when there exists a snapshot + previous to the current.

+ +

+ Snapshot Forward

+ +

This nativates the trace forward one snapshot. All windows displaying machine state will + show that recorded in the current snapshot. This is available only when there exists a snapshot + after the current.

+ +

Emulation Actions

+ +

These actions are visible when the "Control Emulator" mode is selected. They are available + when there is an active trace. Commands are directed to the integrated emulator for the current + trace.

+ +

Resume

+ +

Allow the emulator to resume execution. This is available when no other integrated emulator + is running. A monitor dialog is presented during execution, but the GUI remains responsive. + Only one emulator can be run from the GUI at a time. If the current snapshot represents the + live target, the emulator may read additional machine state from the live target. For + non-contrived programs, the emulator will likely be interrupted, since some instructions and + all system calls are not yet supported. It could also start executing from unmapped memory or + enter a infinite loop. If it seems to carry on too long, interrupt it and examine.

+ +

Interrupt

+ +

Interrupt the currently-running emulator. This is available when any integrated emulator is + running. In most cases, this is the emulator for the current trace, but it may not be. + Canceling the dialog for an emulation task will also interrupt the emulator. Upon interruption, + the emulation schedule is recorded and the snapshot displayed in the GUI.

+ +

Step Back

+ +

Steps the emulator to the previous instruction, by flow. This is available when the current + snapshot includes emulated steps. This operates by repeating the current emulation schedule + with one less step. Thus, it effectively steps backward, heeding the proper control flow. While + not common, if emulation to the current snapshot took a good bit of time, then stepping + backward will likely take about the same amount of time.

+ +

Step Into

+ +

Steps the emulator to the next instruction, by flow. This is available when there is an + active thread. At worst, this operates by repeating the current emulation schedule with one + more step of the current thread. In most cases, this can use the cached emulator for the + current snapshot and advanced it a single step. Note that "Step Over" is not currently + supported by the emulator.

+ +

Skip Over

+ +

Skips the emulator over the current instruction, ignoring flow. This is available when there + is an active thread. At worst, this operates by repeating the current emulation schedule with + an added skip for the current thread. In most cases, this can use the cached emulator for the + current snapshot and advance it by skipping once. Note that this skips the + instruction. Thus, when used on a "call" instruction, all effects and side-effects of the + subroutine are averted. This is not the same as "Step Over," which is not currently + supported by the emulator.

+ +

Recommendations

+ +

Write Target is the default mode, because in most cases, this is the desired behavior. When + the target dies, Write Target essentially means Read Only. For the most part, modifying the + recording itself is discouraged. There are few reasons, perhaps including 1) Hand-generating an + experimental trace; 2) Generating a trace from a script, e.g., importing an event log, + recording an emulated target; 3) Patching in state missed by the original recording. Be wary + when switching from emulation to trace mode, since you could accidentally edit scratch space in + the trace. It is allowed but can produce non-intuitive and erroneous results, since the + emulator caches its snapshots in scratch space.

+ +

To prevent accidental edits to a live target, use the Control Target w/Edits Disabled mode. + This is only effective against edits from the UI, and only when all plugins and scripts use the + state editing service. This cannot prevent a component from accessing Ghidra's Target API to + modify a target. Nor can it prevent edits via the connected debugger's command-line + interpreter. The following components all use the service: Dynamic Listing, Memory (Dynamic + Bytes), Registers, and Watches.

+ + diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/2leftarrow.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/2leftarrow.png new file mode 100644 index 0000000000000000000000000000000000000000..1bd991dba7e60206d2ea1a8321884f5239956824 GIT binary patch literal 2249 zcmYjTX;@Q-61~|2TtX~LZvZ9Wh6pB*3ZmE~2#AWp1D{4@2?&W+Srja*-UPxTB8&1c zxR6u}E=5EOipUa7un1wP&jp?!q=?`GgiV%^+@}BDkD2q$pZR9yeBYdN&TrpNV?%R8 z004}4d3*Tlk@`;%@%kK>oIIlkET6rX4FLCBU2r*{^fe{klM^2h!;4P{I~57ABVxiM zsk@@W_>umRVG&6uUqreA0P*lH5BC1V|Aj9h2vg2N3xM!&hzpQem~lV=n8y+TEZHcx zd9=2WIO6nxzO)$pLjCEC8|KSE5}2DC%=(i_#K~q9iZ&UwBKyweLbg~nhiuA(m2beH ziVno(&?cM#21}l_mbJFZ)Waz?%jYEJ^N?|*8Lxb|G%6F!Rf4wAs2ARAU3M~=2mIF( z9MEB$l94}LY0Q*B^@b& z5QJdI4Hvy@k#!O#wFvB&g~WV6IN~iiT-idUcDBcU!};Z$Gq|`<3Qz?cb)aq}6p)W} z1+?evr7U5pDWm}r5k_VuDy!8-icL)|fkfeX3_;(X^ZNTmtSX4#t*gO~Dh& zgVa*OVs-#oHQj_5^>R2{pH9D`RM&_o&Ovy)up`{WWDP-H*AOGLZb^oCD_B5Sf~*NX z09rNL=UsTM+SBF4?>Sp4RCO}&_)U+Rz^lcj+}hK_Ey4Kcya;KV3#$eME$dkp94cvk znpQYzvLoT&SDY?AIcF<3m%gXTau+B1X_>DNOC0M{5Izk|ykK+#LY?pSFo>0F=lajZms7Kb{R9 z9=svt1dIUH*Up7$4WwPJI!4fs@x>}#ONxZD}}&{yWbAIae8kS2mWA7KokKWuSt zh@hBZ9GIE{q44sUiM(vO3kqpiI88a4w5Gdwr1bzAOMZx1uJ3<1`oyMt@4H78u`?ti zdG$)!ZJgIc!|x-M($J@xQsE^A3HpN|yXz|0P z10LM`4F+9kO#Zz3n`STG`3+h6jF4S#jMA?WD|;;I&n$MTi;AiQ*IUKXtM8-iFl}d3 z6SGufvlgLHk88N_U)3x8`yR4VP4>m-+JLYwA$hB}(OSYrCndAFwF*57pjP2CO?*Qg zQ&W+#t(a|xiF8pkYIFD+7d<1pF-W4wGl>%2G{hB?W)`yYWl}=#yTavS-xjT@e;KW^{FgbEi*r)lY1=r@DlnJ=%Hq&%v0NRqZ4 zh;Kz+6dVPJR1fhI0;3?`KV!!GV4bS%5I(FC0o`Rsb-Jr1?EUS6=9U8?B#vDr{<&b4 z$9kh*Jut3m=fZ|z=i(0sO+t~yZyz1fMRdBMAoArOR!Xq$iVbLXrH=%sX{i$d6W&DpIa&GQ}1!V+T0ctXf~QX%-U(2ei7ZX ztU{KYkzuL`nUQ3a4KB#%kj69fN-C}*u2$$3T?fnvih%UH=%*NYOlAfv*ljQ#i~EOr zz&1oIdp*j=JLgruO&Et^ShFr2+ud=4l?sSAiX*C_SFb&51Q{KMT=(KMNI`VO`{R6( zE=ADnhvpP+#^QB$AlBdGYkix7N|Vc}4xOJd6<)W}r^w1cy*jub^P!rrvS8&UCJ()r z%$bZcywPV?qjU2DOvDs%Xb0ctx~Kbd=a(;BD~TSD_&{xIbY+=aLD-I(lmk+w$ZW-0CW4?7{vKPxi9U$x3;-Nz0&%RO?S^C1n##)8m@{c3-Hm2_f!NrS?gk1fQ7Q zQ;4mDP3&Rd^E`|oKvetEY}PUDy)1;n5%^OZ+^*ezU`%Ea3F_VK2W#jHhC2dXDp*-` zp&{wxl_rF;zQ1l>q?&jf1BSbEtr%?j6-=$*P?m-Gr(WT?>m}Nan2m8$T?0PvqPOGC z4Sv{N@cmaH@h*x;{nd8aRW+v{&L_onvQx(lD+nQ489aAA->iTH#*+nKvX_ zbH?uuA;%2=d`|Y<|HI`NkPzdBc6Nc&qcldp>ou0+ee@YdURb8q&@$ot@iQR4{K7B` z4q`>M0xLCIqcwWntV@*$^0E!gyfEm@QfU1Yt5 zIh8smWu5BnBKBiqH04c2SJo8Wm{yG4-u1`($MZa&=a1))=Xt)*_xqHjtc`Gfk zZofII8kX&VmbEv+XSYwmrOGlAaYOk@3I)mIKSDLbkq3`rP2AADsdNQu;QN4{yTVci z9HF_Kz>nqGGs~=c0n9;bp_h3pxQX@s?20y!)NI&b{X|*zXQ7ig?fGqigRT?ROomfV zadp}Ns&`E4hRrJ_#VYMsNzUi{fTfy;W+2Lm$^&M#LfJUQTPmXL)O>O*t32Z`ADX=$nzOTdzG#fUUyyscw!q3ekZ22FtyKW0 zPUc88uyY*YyBlt{!&{f%tA+>6N-jO~eI`8WC#y;Gtc@^wm$I?D$P?Xg$V@Yi=@L&8DO*9*G@Lj&f_L=|lcMXQ3Ro>kucpc}dT zLJ+n=_sJ7D(VM701}P(BO&W*p{25;wTRD+H0+jfW&PJYf^wGmn6EWjl&DZt1b<0-lD( zoD+SZ`d!P{PgSa1ko?(AT_~M<5$##SZb3Erj2ZG8e&^sfxyEsut7XHjn6vdS_k7u} z6q&460c3upUov|8YHkIuUR#CB#MHelfl3a+^WgF<@@h zd~^ZjSG6!aTI~;qxp4xM_@-p0PD^#Kceewq$>-;zo6H~74Ha7Vbk6Pku~2y&J*6Jw z@zce?ww_~`AEKQ&WxC9?_P`R1>-l?c2T$6~*ZG2;i4(o}tVhbK0Km;@y$)rh#yn2{nJb(^>3h1`BOJ{e|fPDPOdIs<`{&+%y~WJ z{6$Wp<2g$u&g~p}e4S8ToncFKJ6w*l;7u$%;`4d@b!GUopolKbxYkRQx|c+JR>Pvl z7e8ebm-P@q7-wEk445W3xl9{ zwyV}k?nu0as-M8fIK?NuV@A}SJttJ!U`S+hMp!cK9lC_wytwda(~X%u;A`ZF<-%6= z@3J{p`K(5GD$n>`PnC1&6Vo=?Z4-$weB2v_Q9EUavQ)xwI##=(pIl_`)1N#h_+x7P zONSTaE&-b{-ycE0RM#9TiH<$#GL$u2yudMDy(J=Ql7DvOez2!`Q2bSo{k zT$rh?orHr&Wd^)5ER43hvc;;qFC2!wYC;9i&7?J?0`ta(%K_&!;*3I7uOmGWvn$AJ zk}rC5`W7+$O};@Sth(vUaiRg4%P>A5Uj9g?-R*iOPuYEv^v3)as0)H18#-V#8k|@W z$n@zh!Sk%S3$yqIMbfjvXeZ*DfSJk*VNa8e5*Hg8nFL^Z?aWJ)+QLXUcr0qV6>W94 zzrhSv{vrTk_5z{O<<)b8^35?Am7xc(LJ;i@k^6#qJv{auFKJd z=C2-{JAk~5n)p5Ye(CKerNjm495!K4w9ud~)YEFH2HT|d^Wdb?#_}MXHF*u*U>xub zY&&(Su0bLFw7^#p0Y8@99oDZG`H~-)!RurfrroQrlV#)!b9~ML@-bPWTSZae((I$8 zJ+B$yx_B5~O>4@ij$-8OQIBRooKsPfyA>lC2Lesu;KyVzJ6+bNvKCH>Jp;-3z|Z0W zJ~xMFBuW37qTP5-xG+PXh1tp4|?5pudv$q&@hRYZ2d2TMX%jalh3{WR z#;3xs(plI~VAzbD2s8N7QSw*~%By<&h$h2Sq-Ve4FP5NB)&D~tA_XO7he$-enKZg7 zzg;ucJC;g&z!W$)w6r)}%{}IEsWeGp@4PHCdI7YI&S={A(I4qD1z|Y1u*$%P#b}B} zql^9M4Ac1VaI)yv%9@b8yh$|$3`q3r8Bb1`Z-y4c%lN>*{8>yssMZbNx|Kz&-n_p< zqy6;gJ6vD?!Ecr5hq!y=($eNO=S_TQIn$Tn2nLk_hofSGP78JVxcK=<$Q-qvc2CpU z9TzOin{I};>~r?3AiVU4tjX=dQBN$cM=)6mV7H*N{{J$7!0{8^Z!7lZuCsg~K*G|M Ku~%d02mS}lr~3Z@ literal 0 HcmV?d00001 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/disconnect.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/disconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..de33d19e826cba6665cae12e78266d64b0898dfc GIT binary patch literal 556 zcmV+{0@MA8P)4vdJtOz#i5y;I@CgH#nr_r zL!lBE>C*Oa)xV-nt)L-rn>HYY5VA->M2s91(btlKqPExsFO|Cs`E=-|gqU92FFeEh zectc)`N1P{K>!A@48$+06+pRKZv4uu0{}Z^>m74&vVNCQN76$%&eYWS= z!k0HU@DIPg>^6x)XNm2RYxAF!F(}^O&$3gR&NON-YOR02o_Fn}FBG8srry|ndvC#O zIHHU}p&gujn7YqXrPPzC4wiqs&n|ckM~1FU$>iPbl6iA&@AB1Ii4BdOJp&Lw+^@Xb uDW~z=4nJ z@ErkR#;MwT(m+AU64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq=1S7JY5_^ zEP9g@Br*(yU|9)d*<3UE>e+*NO38bf_*c4nJ z@ErkR#;MwT(m+AU64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq=1V2JzX3_ zEPCHg+Q``uAmAK-^FoE`C(%71j{rbWgS!`f0hB yZh7i+HZU%G_DFegg2;itopy594(7M-Pi8k=!<#1@v1c04Xa-MLKbLh*2~7a_utL57 literal 0 HcmV?d00001 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/resume.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/resume.png new file mode 100644 index 0000000000000000000000000000000000000000..b9f6990bff6f8339c5798e3cb860f79e07891f36 GIT binary patch literal 396 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf4nJ z@ErkR#;MwT(m+AU64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<_&IXzt* zLo9le6C^?&{Qv*|A`pL9bLAJ-`aXZv>!j~+4yonP(3J2;X zB&8&ETgTe~DWM4fleDxa literal 0 HcmV?d00001 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/skipover.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/skipover.png new file mode 100644 index 0000000000000000000000000000000000000000..3dd8717a12bc43ab1e0ef873053a5115e6ed7f84 GIT binary patch literal 497 zcmV-hZWsJS&7jSEGmp{vQozNXId!gN`iBWoO>q{eVf~#9z)(@BjE&%LcmKh)U-V9({Fg`*rkZEzP z22neR$}V%=^tlY1`(xya8sRY<2H nnS^-!hlO?D0)m7(E-#FC$+W!}D)Jep00000NkvXXu0mjf5rE6I literal 0 HcmV?d00001 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepback.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepback.png new file mode 100644 index 0000000000000000000000000000000000000000..a1fad09fe5068ccc05c5979ac3aeb00564c3560f GIT binary patch literal 428 zcmV;d0aN~oP)L3DBGV5h8h zOqVXM4$?(Mw=6CWEr^L@5vTTFi3F6MOA_Nri^0J+9q-=z?#JE1e@hta4zTl!-U|={ z+A5fxi!&y>+f=1kQ3IA5o?m(Q&e;c^WMQw_Zmcf@NCE8zRiFt_oS-gKoPEM9PKeC% z)&^>Ji=F1`CP0Yv^f+Mx3{60sFa+#0Cjg`wDk}@{fK8xP@LRwDcmpnhk5vE(%ul4nJ z@ErkR#;MwT(m+AU64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<_&1wCCH zLo9laPBQdoaujIYf7klrgZPab8z1%DY!cw+PIAo1@tG2lk(AoGcAE$bmuX0wle9EX zl|*jO#>eY3d~71Rh4&oZ*?ew#&L->kdvDIX+4{eY=^BH+>u0VX4O2bDHF&J9s1&6< z{Mfs`+1RXs-E;BfQopHtn$Kppebwf2u$Q{dV8&Pf z`^Cx3yD4gmPT%HPdbj#`)NA+FjbbHRjSYKdr8X@%eY8@r;p|+!V>9ffGTHXmN-*DX u{bnR2e8#A6p~9SlK1)-=v_96K>CI>R8-6o7JSh|yf()LnelF{r5}E+XIGu0+ literal 0 HcmV?d00001 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/steplast.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/steplast.png new file mode 100644 index 0000000000000000000000000000000000000000..0c5c187b871c1c594dfdb1f51e4b0451fcc1b370 GIT binary patch literal 410 zcmV;L0cHM)P)mAtX6-4nLEmXwDTM0(Qtc6=_77YnGFfhz~-+Rs+<}hUhnf(eD$MC)22JuYl zaOLo9d!u9ildAq=RNsfT$=>VhqMZfw`r4=@x&XqgXwn!5ke>z+VRu}>L$Rq-Z0g_vQES~5=0*Y1 zO}tDNM0*=9w$_Blrd_FV}F$32Ssy1tN*l5`2YX_07*qoM6N<$ Eg1^$FbpQYW literal 0 HcmV?d00001 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepout.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepout.png new file mode 100644 index 0000000000000000000000000000000000000000..1e8886167a6797cd93aa6d7e7afcd23865d39728 GIT binary patch literal 413 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf4nJ z@ErkR#;MwT(m+AU64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<_&r9536 zLo9mtPBippb`)vZA6vXa;U9x^^+C4)4#y1^E-pcxQ(I#dwl3(+S$^Y|tK!^_b|sF+ zhK^R2%hgNwD^6%wap%?M9V!L8ZVIq0k7_gjeDBO5`{YOJhGvTn>KU-^`NCoQiuJek zm4>Zy6F>84CopTAIcG38c*QyC?%S_3*6y@j6U6fBRPu(ZG6!Az_i;sW>Xxg5zQk_W zU^2aHZL7Z{!!c$Nht@dHqBj5l literal 0 HcmV?d00001 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepover.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepover.png new file mode 100644 index 0000000000000000000000000000000000000000..72d995c31876b3620793c9f257f74042445b0d61 GIT binary patch literal 428 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf4nJ z@ErkR#;MwT(m+AU64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<_&H9cJ% zLo9laPBQdAn|mr~ z4NGK3Mcvc8yA$Lqm?or!S}s1N;R4f!+}XR2+cB)mW>%=KsC|boFyt=akR{0DH-#761SM literal 0 HcmV?d00001 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-disabled.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..bad4477d9617023d369c4280928e3a44d55c5d68 GIT binary patch literal 733 zcmV<30wVp1P)+lt(g(khG`c?H-cRh)Wt$6H>X!QFj_eaQGZ<=T`GFuz~On` z=Xp5ieUGIb7LX_DxTJ&Y(5$4lmSycZU0u|jW!b-8eTv_Z^wZiVEKn?VM}fdOjgH!y zo;E~DiPX@rHgTk6r7V%N_irk#j`uyO$meL;X0eB0Fp1L%(#|_Kmvmr&A6+9XTo2)| zIXsw2CKya&S*FnEgR!wBb8`bMEOal2!;>^M8P2vz5B78OY%?dSi|!hOZ{HqmNiHvk zSS$qCl62I&uRj&N2)Az;-sR#$`2KH6`2zB2ZC%YImzQrbDJ(23xO!oKjHm|^m%#4_ ziM95&HNYjHh001J8XHRziw$K=yk0(rpG+sm%OL78`}IS^)zko-5pa#{Yy-~C^#eBX z_+u(63|3kSlYJ2NLe#_3RIkCEPJk=6B*|Luro*vlS=O40i_a?(38|t&nwpZDyQHkk zl59P7LN~-n`?;9&L!Z~x<|cj+q`_x``<(Yw;K|Pfz4Bx zPbf5xZJPqWA0m-2lM@p!p7z{ZU5_~A#K87sYZoJzq+^l}twRY(?<~toZLj?Yn&IE(6HHP^ P00000NkvXXu0mjfjrvKk literal 0 HcmV?d00001 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-emulator.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-emulator.png new file mode 100644 index 0000000000000000000000000000000000000000..53964c75edb83979b472dcc8cab96aea481e1ddd GIT binary patch literal 826 zcmV-A1I7G_P)e?HE&8I(gKOP6N#C0m8>wGrBa%I(1d*pH#e>6*!Jyokz3Xb zI(O$i&+okFIUJ6NsI|2f0|NtCD{^ylQC?mS0Eak^`&g8fYT`Mz*fls+1HjFTqIg{t z#V}wn7@*N;V6j*L05+QqU0q$s$jBI<@9XV&c(8Wwk*)i4-JI?Gtkc!b{{E4OKtvIf z)L$(tPT8>-fB#blqIe#S+KTmg>6;q3|FNLc>0qqMw%@LdRXbb~TG{~j%oYqertUES z<2s!V3WXv976bt$B_%L5=@+Wc#Hay470=^J6^|v=;LNKJ(>ED_GD(tre!m}#F~r2g zAUZl4j4}9pJ~$i>U%=JlQN{D&#!j~cpM1ASMA_Vm?+g9?{mo9Nldpba2LKpPtD#b< z0?kd8ea4G(n#5HN>K`j`?s7M*Rx4ygq@A`c-rbm-&Odu=4U2ld2so9JAlt6~p0^ry zxFqzAaF}ms!1D4kxXw3|Vr@&WS}B*o8G!2$?^ z005B9xt7!%hX@z|yuJV=wgGO%0YnAA!ef7l(ODZ((uA;vl@(`Vol+itcv@G4_G?Aq zyzW*TK99}d{qPbl8J$ok?}5Q!fL^cvHNnaf!$6P#;CDVWGKrICZy+aYC)7zf2;Rh(!HzFyP);u<*ic^Y2K(ru>D8?lwA)%lF`P#z3zm83I zRGW5eSDNazjAwGwb1S;vO#=W5_obrpcn$(m(6?>#`U<<f{xN+pO0UauD+ zPx(YNMnsfZ+D4hBZL|`jM5GC6EEbE+Y&M5#WHy`WC;Hb-JMz+Ev;Y7A07*qoM6N<$ Ef;S><;s5{u literal 0 HcmV?d00001 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-target.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-target.png new file mode 100644 index 0000000000000000000000000000000000000000..37b05b2d51b3b3eaecc88ce360779297c2d71f5d GIT binary patch literal 694 zcmV;n0!jUeP)KX;B~3wK*AsVs;zybg613`Q?K7(w7m zKDdIQWP%}t8}^SPeaJ%Sr5-|jvxjIBLOmAgieMuu?!cSTN=O*!NVyL3UwAz|s8<*6 zp$`1uoWuEkzQ4ndAN*VRPB$ES(scB2Kd=ixDKh8=P6Abc029E+^PywL!xLTJ<&_m~ zryk{Bq>Y-t0g9@s*CLTfPfJUStG2cl0K+io?Y^{9U;fMK&R5f3a}I~1aPd|5W9L?} zrfI|Rc>GKt5GbHlR#pq|2n1ky8k(CSp9f&pvT#(H_`E*bYlDrzN}}TX+qy0jT!-{cBk=X%Eg=7D^rL z@s@1%Op6lrIKbYm0AZUjmX@e*3iI1hW%mZ2ySdPI)7ESNGUMapYgs5%0I?k9%$2rc zmbQ~M55mq6xm*q)vlTEBkH;4Qh{eF;u>n2z2Kn-Jk`H4G+#hssxvP(%p`is}w7^MK z)vLO$Z$4uzCJ9XwKo0EREq8CU%j)W?=(;W*k0-X>%8I6G!!|@j5{ZOV?DNXoH!np* zMAvoEH0>G1zW_>ES=p6nG&-M3r6iNdNH&|5WHKqyXmozvxjt<807*qoM6N<$f`yha?f?J) literal 0 HcmV?d00001 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-trace.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-trace.png new file mode 100644 index 0000000000000000000000000000000000000000..f3c62d5a2cb556734b68bec4324c19623445d929 GIT binary patch literal 865 zcmV-n1D^beP)QoS26h#+ZL|1(g6`^F3Ar)c8<`x9)xC^^&AL>P5fzXUtC>nvF zbH+LTyC|pqfUW2HeV*s#ecng#FH}`kZjnd>fghL*3?c~$L}1UJ{nyG%ifUD%y!orW zcOQ@tP*e@U7DTAaIp2v&>#0Zb@d}z zG#hKz&zm)S7AZT)(AxxWhKCqQS_mPy{`&>K(89`tXD?V*xQK6Om*ut%g$gtPM^b03 zH423a54$=6@a6l^Gz~j#6Ap(_QujEzH-}ifhr0TsB%(91r4v`Gsw%fEDOps_jQ93$0X=-c+psBH$UoX|NYl8%!AYbLcZk1%=!&uY(z6wPm4PDn6w%!8J)%A!w zEqAeoEdXLKV@%EKr66C;-sE+{;dx?u5{M}X{GL1yb~=SG-$yc;MAwV?Y0ZxSq^&3i zwt4@N)aeEn3;f^Z0canh!WnbWG>z|nD9;YY;|V6IT}=I(do1qY-7OBz#xFC zsAMu3Zd|`f;A#W5ZPU};gMZm-(#qVA?g5R>TlX@~(~yL$5v$yllgq%s0B*ONP_PYv zqB%u4N+Ai?Y9@?5!Ro(3N}EEi+Xa#kfFck9(=_pVCIV1gJdc%MufXGd|44@_>oFfi z33iNdat;Ko#r={1$jA9hhId5Pzv3SB?vS8Pj~6I9Wr@smm}XsK0b^ z@M-jEtzj6Y0RP*C{{DX4Za3Nl4bwDfY`WDm@ak1#I%Thtf)?Q@7qSB5_(U5o0vLu- riZXUq2yyK{0-uZZ8`m9_QZ9Z6zoS45?C?2p00000NkvXXu0mjfsX2%X literal 0 HcmV?d00001 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html index b9633e8e18..91e5ca6265 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html @@ -58,9 +58,9 @@ failed, the first address in the failed range is displayed with a pink background. Otherwise, up-to-date contents are displayed with the default background color.

-

The dynamic listing supports editing memory via the Machine State Editing - Plugin and Service. Such edits are performed as usual: Via the The dynamic listing supports editing memory. See Control and Machine State. + Such edits are performed as usual: Via the Patch actions, or by pasting byte strings. These edits may be directed toward a live target, the trace, or the emulator.

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerMemoryBytesPlugin/DebuggerMemoryBytesPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerMemoryBytesPlugin/DebuggerMemoryBytesPlugin.html index 8db10e4bb6..1021f472cc 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerMemoryBytesPlugin/DebuggerMemoryBytesPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerMemoryBytesPlugin/DebuggerMemoryBytesPlugin.html @@ -44,13 +44,12 @@ failed, the first address in the failed range is displayed with a pink background. Otherwise, up-to-date contents are displayed with the default background color.

-

The dynamic listing supports editing memory via the Machine State Editing - Plugin and Service. Such edits are performed as usual: Toggling edits and typing into the - editor, or by pasting byte strings. These edits may be directed toward a live target, the - trace, or the emulator. NOTE: Please be wary of hand-typing large edits into the - emulator, since every keystroke may produce a unique scratch snapshot. It is better to paste - such edits instead.

+

The dynamic listing supports editing memory. SeeControl and Machine State. + Such edits are performed as usual: Toggling edits and typing into the editor, or by pasting + byte strings. These edits may be directed toward a live target, the trace, or the emulator. + NOTE: Please be wary of hand-typing large edits into the emulator, since every keystroke + may produce a unique scratch snapshot. It is better to paste such edits instead.

Actions

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html index e03a7277b2..17ce19b2e0 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html @@ -126,7 +126,7 @@

Interrupt the current target's execution.

-

Resume (Continue, Go)

+

Resume (Continue, Go)

Allow the current target to resume execution.

@@ -142,7 +142,7 @@

Allow the current target to finish the current subroutine, pausing after.

-

Step Last / Extended

+

Step Last / Extended

Perform a target-defined step, often the last (possibly custom or extended) step.

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html index 71cb6fa711..2d86a9a2d6 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html @@ -46,9 +46,7 @@
  • Value - the value of the register as recorded in the trace. When the value refers to a valid memory offset, right-clicking the row allows the user to navigate to that offset in a selected memory space. This field is user modifiable when the Enable Edits toggle is - on, and the editing service - reports the register as modifiable. Edits may be directed toward a live target, the trace, or + on, and the register is modifiable. Edits may be directed toward a live target, the trace, or the emulator. Values changed by the last event are displayed in red.
  • @@ -102,9 +100,9 @@

    This toggle is a write protector for machine state. To modify register values, this toggle must be enabled. Edits are directed according the to State Editing Plugin - and Service. Note: Only the raw "Value" column can be edited directly. The "Repr" - column cannot be edited, yet.

    + "help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html">Control and Machine State + Plugin. Note: Only the raw "Value" column can be edited directly. The "Repr" column + cannot be edited, yet.

    Clone Window

    diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/DebuggerStateEditingPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/DebuggerStateEditingPlugin.html deleted file mode 100644 index 933d551711..0000000000 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/DebuggerStateEditingPlugin.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - Debugger: Editing Machine State - - - - - -

    Debugger: Editing Machine State

    - -

    This plugin controls the modification of machine state. It provides a multi-state action in - the main toolbar for controlling the editing mode of each trace and associated target. It is - backed by a corresponding service plugin which manages the editing modes and dispatches - machine-state edits accordingly. Scripts can also use the service to perform machine-state - edits that behave consistently with the rest of the UI.

    - -

    Actions

    - -

    The plugin provides a single action:

    - -

    Edit Mode

    - -

    This action is available whenever a trace is active. It changes the machine-state editing - mode for the active trace and, if applicable, its associated target. The possible modes - are:

    - -
      -
    • Read-Only - Rejects all edits.
    • - -
    • Write Target - The default, this directs edits - to the live target. To accept changes, the UI must be "live and at the present." If the trace - has no associated target, i.e., it is dead; or if the current view is in the past or includes - any steps of emulation, i.e., it is not at the present; then edits are rejected.
    • - -
    • Write Trace - Directs all edits to the trace. - Edits are generally always accepted, and they are applied directly to the trace.
    • - -
    • Write Emulator - Materializes edits via - emulation. Instead of editing the trace, this generates a patch and appends it to the current - coordinates' emulation schedule. See the Go To Time - action. Essentially, the change is applied in the trace's scratch space, leaving the original - recording in tact. Due to implementation details, a thread must be selected, even if edits - only affect memory. Additionally, the disassembly context register cannot be modified.
    • -
    - -

    Recommendations

    - -

    Write Target is the default mode, because in most cases, this is the desired behavior. When - the target dies, Write Target essentially means Read Only. For the most part, modifying the - recording itself is discouraged. There are few reasons, perhaps including 1) Hand-generating an - experimental trace; 2) Generating a trace from a script, e.g., importing an event log, - recording an emulated target; 3) Patching in state missed by the original recording. More often - than not, when experimenting with the emulator, the mode should be Write Emulator. Using Write - Trace with the emulator will almost certainly result in issues with cache staleness.

    - -

    Some background and an example: To display emulated machine state, the emulator executes a - specified schedule and writes the resulting state into the trace's scratch space, keyed by the - schedule. Suppose you emulate a step forward but then realize that some state was incorrect, or - you just want to try the same step with an alternative initial state. If you step back then - edit the trace and then repeat the step forward, the UI will simply recall the cached snapshot, - rendering the state change ineffective. Instead, use Write Emulator to edit the state and then - step forward. Because the patch is encoded in the emulation schedule, the UI will not recall - the stale snapshot. Instead the emulator will execute the new schedule and generate a new - scratch snapshot. Furthermore, the original trace recording remains in tact, while the modified - state is stored in scratch space with the schedule explaining where it came from. Still better, - the first scratch snapshot (for the step taken without first modifying the state) also remains - in tact, and the two can be compared.

    - -

    To prevent edits to a live target, use the Read-Only mode. This will prevent most accidental - edits. This is only effective against edits from the UI, and only when all plugins and scripts - use the state editing service. This cannot prevent a component from accessing Ghidra's Target - API to modify a target. Nor can it prevent edits via the connected debugger's command-line - interpreter. The following components all use the service: Dynamic Listing, Memory (Dynamic - Bytes), Registers, and Watches.

    - - diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-disabled.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-disabled.png deleted file mode 100644 index 483d165a279de306a2a176f92fe53b7bdab7464f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 397 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf4nJ z@ErkR#;MwT(m+AU64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<_&xjbDQ zLo9lGC!OwXb`Ws6A2GXm>n(wrSqc;Q`Iwux2c)x32C41X>+*jxT|s@dFd{d znUMvXl6for80RqVt7qI$Vp@E-@d|&A{2ZRE_lqwa4%J;!eeQ_Aae~@|Io(1rSNqfzN pKb}~YQTrp%_wtdG>mUAOm~d6n!aVzWEHLO8JYD@<);T3K0RUp+pSJ)2 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-emulator.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-emulator.png deleted file mode 100644 index 72600b472c1ed05300c0504626d3cada96d43fdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 559 zcmV+~0?_@5P)MTn`DJGE*L$$@C;s&`@2~|Bsp}0Z5W$uBu10 z$J{x03}{f3Dv7PNs#^C{btobRSd^s^Fw&Eg8l9$Ek(gaM5ivzk$X-27TkRR=oQTL- zP+J&dzE&6K9Nv4>_ghI!Y5O&>ZV^}m^l8|K;2Ri#D2ig>3a|k#%d>ILb-edm@cWvl z>0maS{Sc93V7gony36&T3rs_GxqHrw6s)x`E3mb8A^^8XA8zGnPRv`BBY^Lx&+y)F z3DS34f(@|Z|8Kn>A3nsM002ovPDHLkV1g4+>SX`` diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-target.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-target.png deleted file mode 100644 index c8f0e047ecbc9bbcaf62c7cc291bf692383ebcc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 414 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf4nJ z@ErkR#;MwT(m+AU64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<_&r9E97 zLo9laPBip8>>%JeUohKzqU(I$$;Y(0IcGW-F!l*3NgiULXx zw(z)?$qHX<>C>;xEHkP9CvW}e{e(E#zZ%~NN!m8JbKN9L_KPUHFm09QRne`p9M^ryN{PCiy`P~0@eT((=cP$Up6sM_=GHGjbWori)h$Rk0v0x5{1g zXLvjft82^04L+U{N@Y}dWsh(;%uO-79pvR4P0aB#X|TLtSk>h1~)T4GmaUu8Qcp7j_HO)z;(eIUrdCPy`}rSr(ca z0idU+n;SQ;pLD%ad;0SS05mm%WmyPB5FnsDrlyAW_I3cWR)$P= zf!A+d@_lKU&W$Zf&FpO&`Value - the raw bytes of the watched buffer. If the expression is a register, then this is its hexadecimal value. This field is user modifiable when the Enable Edits toggle - is on, and the editing service - reports the register as modifiable. Edits may be directed toward a live target, the trace, or - the emulator. If the value has changed since the last navigation event, this cell is rendered - in red. + is on, and the variable is modifiable. Edits may be directed toward a live target, the trace, + or the emulator. If the value has changed since the last navigation event, this cell is + rendered in red.
  • Type - the user-modifiable type of the watch. Note the type is not marked up in the trace. Clicking the Apply Data Type action will apply it to the current trace, if @@ -157,9 +155,9 @@

    This toggle is a write protector for machine state. To modify a watch's value, this toggle must be enabled. Edits are directed according the to State Editing Plugin - and Service. Note: Only the raw "Value" column can be edited directly. The "Repr" - column cannot be edited, yet.

    + "help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html">Control and Machine State + Plugin. Note: Only the raw "Value" column can be edited directly. The "Repr" column + cannot be edited, yet.

    Tool Options: Colors

    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 f205a6c068..ac05c89b52 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 @@ -709,7 +709,7 @@ public class DebuggerCoordinates { } public boolean isAlive() { - return recorder != null; + return recorder != null && recorder.isRecording(); } public boolean isPresent() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java index 5ab2b73766..509ec8d339 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java @@ -22,15 +22,12 @@ import java.util.List; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.swing.*; import docking.action.DockingAction; import docking.action.ToggleDockingAction; import docking.action.builder.*; -import docking.menu.ActionState; import docking.widgets.table.*; import docking.widgets.tree.GTreeNode; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; @@ -51,7 +48,6 @@ import ghidra.app.plugin.core.debug.gui.thread.DebuggerThreadsPlugin; import ghidra.app.plugin.core.debug.gui.time.DebuggerTimePlugin; import ghidra.app.plugin.core.debug.gui.watch.DebuggerWatchesPlugin; import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; import ghidra.app.services.DebuggerTraceManagerService.BooleanChangeAdapter; import ghidra.async.AsyncUtils; import ghidra.framework.plugintool.Plugin; @@ -183,10 +179,10 @@ public interface DebuggerResources { ImageIcon ICON_EDIT_MODE_WRITE_EMULATOR = ResourceManager.loadImage("images/write-emulator.png"); - String NAME_EDIT_MODE_READ_ONLY = "Read Only"; - String NAME_EDIT_MODE_WRITE_TARGET = "Write Target"; - String NAME_EDIT_MODE_WRITE_TRACE = "Write Trace"; - String NAME_EDIT_MODE_WRITE_EMULATOR = "Write Emulator"; + String NAME_EDIT_MODE_READ_ONLY = "Control Target w/ Edits Disabled"; + String NAME_EDIT_MODE_WRITE_TARGET = "Control Target"; + String NAME_EDIT_MODE_WRITE_TRACE = "Control Trace"; + String NAME_EDIT_MODE_WRITE_EMULATOR = "Control Emulator"; HelpLocation HELP_PACKAGE = new HelpLocation("Debugger", "package"); @@ -2089,26 +2085,6 @@ public interface DebuggerResources { } } - interface EditModeAction { - String NAME = "Edit Mode"; - String DESCRIPTION = "Choose what to edit in dynamic views"; - String GROUP = GROUP_GENERAL; - Icon ICON = StateEditingMode.values()[0].icon; - String HELP_ANCHOR = "edit_mode"; - - static MultiStateActionBuilder builder(Plugin owner) { - String ownerName = owner.getName(); - return new MultiStateActionBuilder(NAME, ownerName) - .description(DESCRIPTION) - .toolBarGroup(GROUP) - .toolBarIcon(ICON_EDIT_MODE_WRITE_TARGET) - .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)) - .addStates(Stream.of(StateEditingMode.values()) - .map(m -> new ActionState<>(m.name, m.icon, m)) - .collect(Collectors.toList())); - } - } - interface LimitToCurrentSnapAction { String NAME = "Limit to Current Snap"; String DESCRIPTION = "Choose whether displayed objects must be alive at the current snap"; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java new file mode 100644 index 0000000000..29f25d3761 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java @@ -0,0 +1,1067 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.control; + +import java.awt.event.KeyEvent; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.swing.Icon; +import javax.swing.KeyStroke; + +import com.google.common.collect.Range; + +import docking.ActionContext; +import docking.DockingContextListener; +import docking.action.DockingAction; +import docking.action.DockingActionIf; +import docking.action.builder.*; +import docking.menu.ActionState; +import docking.menu.MultiStateDockingAction; +import docking.widgets.EventTrigger; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.debug.*; +import ghidra.app.plugin.core.debug.event.*; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.service.emulation.DebuggerPcodeMachine; +import ghidra.app.services.*; +import ghidra.app.services.DebuggerEmulationService.CachedEmulator; +import ghidra.app.services.DebuggerEmulationService.EmulatorStateListener; +import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; +import ghidra.app.services.DebuggerStateEditingService.StateEditingModeChangeListener; +import ghidra.async.AsyncUtils; +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.target.TargetSteppable.TargetStepKind; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.trace.model.Trace; +import ghidra.trace.model.Trace.TraceObjectChangeType; +import ghidra.trace.model.TraceDomainObjectListener; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectValue; +import ghidra.trace.model.time.schedule.Scheduler; +import ghidra.util.*; + +@PluginInfo( + shortDescription = "Debugger global controls", + description = "GUI to control target, trace, and emulator; and edit machine state", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, + eventsConsumed = { + TraceActivatedPluginEvent.class, + TraceClosedPluginEvent.class, + ModelObjectFocusedPluginEvent.class, + }, + servicesRequired = { + DebuggerStateEditingService.class, + DebuggerTraceManagerService.class, + }) +public class DebuggerControlPlugin extends AbstractDebuggerPlugin + implements DockingContextListener { + + static String intSubGroup(int subGroup) { + return String.format("%02d", subGroup); + } + + abstract class TargetAction extends DockingAction { + TraceObject object; + + public TargetAction(String name) { + super(name, DebuggerControlPlugin.this.getName()); + } + + abstract Class getTargetInterface(); + + TraceObject findTraceObject() { + TraceObject focus = current.getObject(); + if (focus == null) { + return null; + } + return focus.querySuitableTargetInterface(getTargetInterface()); + } + + T getTargetObject(TraceObject object) { + TraceRecorder recorder = current.getRecorder(); + if (recorder == null || !recorder.isRecording()) { + return null; + } + Class iface = getTargetInterface(); + if (object != null) { + return iface.cast(recorder.getTargetObject(object)); + } + return recorder.getFocus().getCachedSuitable(iface); + } + + abstract boolean isEnabledForObject(T t); + + abstract void actionPerformed(T t); + + @Override + public boolean isEnabledForContext(ActionContext context) { + object = findTraceObject(); + T t = getTargetObject(object); + if (t == null || !t.getModel().isAlive()) { + return false; + } + return isEnabledForObject(t); + } + + @Override + public void actionPerformed(ActionContext context) { + T t = getTargetObject(object); + if (t == null) { + return; + } + actionPerformed(t); + } + } + + static class TargetActionBuilder + extends AbstractActionBuilder, ActionContext, TargetActionBuilder> { + final DebuggerControlPlugin owner; + + @SuppressWarnings("unchecked") + Class iface = (Class) TargetObject.class; + Function> actionCallback; + BiPredicate enabledPredicate; + + public TargetActionBuilder(String name, DebuggerControlPlugin owner) { + super(name, owner.getName()); + this.owner = owner; + } + + @SuppressWarnings("unchecked") + public TargetActionBuilder withInterface(Class iface) { + this.iface = (Class) iface; + return (TargetActionBuilder) self(); + } + + public TargetActionBuilder enabledWhenTarget( + BiPredicate enabledPredicate) { + this.enabledPredicate = enabledPredicate; + return self(); + } + + public TargetActionBuilder onTargetAction( + Function> actionCallback) { + this.actionCallback = actionCallback; + return self(); + } + + @Override + protected TargetActionBuilder self() { + return this; + } + + @Override + public TargetAction build() { + Objects.requireNonNull(iface, "Must specify withInterface"); + Objects.requireNonNull(enabledPredicate, "Must specify enabledWhenTarget"); + Objects.requireNonNull(actionCallback, "Must specify onTargetAction"); + + TargetAction action = owner.new TargetAction<>(name) { + @Override + Class getTargetInterface() { + return iface; + } + + @Override + boolean isEnabledForObject(T t) { + return enabledPredicate.test(object, t); + } + + @Override + void actionPerformed(T t) { + actionCallback.apply(t).exceptionally(ex -> { + owner.tool.setStatusInfo(name + " failed: " + ex.getMessage(), true); + Msg.error(this, name + " failed", ex); + return null; + }); + } + }; + decorateAction(action); + return action; + } + } + + interface ControlAction { + String GROUP = DebuggerResources.GROUP_CONTROL; + } + + interface EditModeAction { + String NAME = "Edit Mode"; + String DESCRIPTION = "Choose what to edit in dynamic views"; + String GROUP = DebuggerResources.GROUP_CONTROL; + String HELP_ANCHOR = "edit_mode"; + + static MultiStateActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new MultiStateActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(DebuggerResources.ICON_BLANK) // Docs say required + .toolBarGroup(GROUP, "") + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)) + .addStates(Stream.of(StateEditingMode.values()) + .map(m -> new ActionState<>(m.name, m.icon, m)) + .collect(Collectors.toList())); + } + } + + interface ResumeAction extends ControlAction { + Icon ICON = DebuggerResources.ICON_RESUME; + int SUB_GROUP = 0; + KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0); + } + + interface TargetResumeAction extends ResumeAction { + String NAME = "Resume Target"; + String DESCRIPTION = "Resume, i.e., go or continue execution of the target"; + String HELP_ANCHOR = "target_resume"; + + static TargetActionBuilder builder(DebuggerControlPlugin owner) { + String ownerName = owner.getName(); + return new TargetActionBuilder<>(NAME, owner) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)) + .withInterface(TargetResumable.class); + } + } + + interface EmulateResumeAction extends ResumeAction { + String NAME = "Resume Emulator"; + String DESCRIPTION = "Resume, i.e., go or continue execution of the integrated emulator"; + String HELP_ANCHOR = "emu_resume"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface InterruptAction extends ControlAction { + Icon ICON = DebuggerResources.ICON_INTERRUPT; + int SUB_GROUP = 1; + KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_I, KeyEvent.CTRL_DOWN_MASK); + } + + interface TargetInterruptAction extends InterruptAction { + String NAME = "Interrupt Target"; + String DESCRIPTION = "Interrupt, i.e., suspend, the target"; + String HELP_ANCHOR = "target_interrupt"; + + static TargetActionBuilder builder(DebuggerControlPlugin owner) { + String ownerName = owner.getName(); + return new TargetActionBuilder<>(NAME, owner) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)) + .withInterface(TargetInterruptible.class); + } + } + + interface EmulateInterruptAction extends InterruptAction { + String NAME = "Interrupt Emulator"; + String DESCRIPTION = "Interrupt, i.e., suspend, the integrated emulator"; + String HELP_ANCHOR = "emu_interrupt"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface TargetKillAction extends ControlAction { + Icon ICON = DebuggerResources.ICON_KILL; + String HELP_ANCHOR = "target_kill"; + int SUB_GROUP = 2; + String NAME = "Kill Target"; + String DESCRIPTION = "Kill, i.e., forcibly terminate the target"; + KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_K, + KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK); + + static TargetActionBuilder builder(DebuggerControlPlugin owner) { + String ownerName = owner.getName(); + return new TargetActionBuilder<>(NAME, owner) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)) + .withInterface(TargetKillable.class); + } + } + + interface DisconnectAction extends ControlAction { + String NAME = "Disconnect"; + String DESCRIPTION = "Close the connection to the debugging agent"; + Icon ICON = DebuggerResources.ICON_DISCONNECT; + String HELP_ANCHOR = "target_disconnect"; + int SUB_GROUP = 3; + KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_K, + KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK); + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface EmulateStepBackAction extends ControlAction { + String NAME = "Step Emulator Back"; + String DESCRIPTION = "Step the integrated emulator a single instruction backward"; + Icon ICON = DebuggerResources.ICON_STEP_BACK; + String HELP_ANCHOR = "emu_step_back"; + int SUB_GROUP = 4; + KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0); + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface StepIntoAction extends ControlAction { + Icon ICON = DebuggerResources.ICON_STEP_INTO; + int SUB_GROUP = 5; + KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0); + } + + interface TargetStepIntoAction extends StepIntoAction { + String NAME = "Step Target Into"; + String DESCRIPTION = "Step the target a single instruction, descending into calls"; + String HELP_ANCHOR = "target_step_into"; + + static TargetActionBuilder builder(DebuggerControlPlugin owner) { + String ownerName = owner.getName(); + return new TargetActionBuilder<>(NAME, owner) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)) + .withInterface(TargetSteppable.class); + } + } + + interface EmulateStepIntoAction extends StepIntoAction { + String NAME = "Step Emulator Into"; + String DESCRIPTION = + "Step the integrated emulator a single instruction, descending into calls"; + String HELP_ANCHOR = "emu_step_into"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface TargetStepOverAction extends ControlAction { + String NAME = "Step Target Over"; + String DESCRIPTION = "Step the target a single instruction, without following calls"; + Icon ICON = DebuggerResources.ICON_STEP_OVER; + String HELP_ANCHOR = "target_step_over"; + int SUB_GROUP = 6; + KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F10, 0); + + static TargetActionBuilder builder(DebuggerControlPlugin owner) { + String ownerName = owner.getName(); + return new TargetActionBuilder<>(NAME, owner) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)) + .withInterface(TargetSteppable.class); + } + } + + interface EmulateSkipOverAction extends ControlAction { + String NAME = "Skip Emulator"; + String DESCRIPTION = + "Skip the integrated emulator a single instruction, ignoring its effects"; + Icon ICON = DebuggerResources.ICON_SKIP_OVER; + String HELP_ANCHOR = "emu_skip_over"; + int SUB_GROUP = 7; + KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F10, KeyEvent.CTRL_DOWN_MASK); + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface TargetStepFinishAction extends ControlAction { + String NAME = "Step Target Finish"; + String DESCRIPTION = "Step the target until it completes the current frame"; + Icon ICON = DebuggerResources.ICON_STEP_FINISH; + String HELP_ANCHOR = "target_step_finish"; + int SUB_GROUP = 8; + KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F12, 0); + + static TargetActionBuilder builder(DebuggerControlPlugin owner) { + String ownerName = owner.getName(); + return new TargetActionBuilder<>(NAME, owner) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)) + .withInterface(TargetSteppable.class); + } + } + + interface TargetStepLastAction extends ControlAction { + String NAME = "Step Target Repeat Last"; + String DESCRIPTION = "Step the target in a target-defined way"; + Icon ICON = DebuggerResources.ICON_STEP_LAST; + String HELP_ANCHOR = "target_step_last"; + int SUB_GROUP = 9; + KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, KeyEvent.CTRL_DOWN_MASK); + + static TargetActionBuilder builder(DebuggerControlPlugin owner) { + String ownerName = owner.getName(); + return new TargetActionBuilder<>(NAME, owner) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)) + .withInterface(TargetSteppable.class); + } + } + + interface TraceSnapBackwardAction extends ControlAction { + String NAME = "Trace Snapshot Backward"; + String DESCRIPTION = "Navigate the trace recording backward one snapshot"; + Icon ICON = DebuggerResources.ICON_SNAP_BACKWARD; + String HELP_ANCHOR = "trace_snap_backward"; + int SUB_GROUP = 10; + KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0); + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface TraceSnapForwardAction extends ControlAction { + String NAME = "Trace Snapshot Forward"; + String DESCRIPTION = "Navigate the trace recording forward one snapshot"; + Icon ICON = DebuggerResources.ICON_SNAP_FORWARD; + String HELP_ANCHOR = "trace_snap_backward"; + int SUB_GROUP = 11; + KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0); + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, intSubGroup(SUB_GROUP)) + .keyBinding(KEY_BINDING) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + private final TraceDomainObjectListener listenerForObjects = new TraceDomainObjectListener() { + { + listenFor(TraceObjectChangeType.VALUE_CREATED, this::valueChanged); + listenFor(TraceObjectChangeType.VALUE_DELETED, this::valueChanged); + listenFor(TraceObjectChangeType.VALUE_LIFESPAN_CHANGED, this::valueLifespanChanged); + } + + private void valueChanged(TraceObjectValue value) { + if (value.getLifespan().contains(current.getSnap())) { + Swing.runIfSwingOrRunLater(() -> updateActionsEnabled()); + } + } + + private void valueLifespanChanged(TraceObjectValue value, Range oldLife, + Range newLife) { + if (newLife.contains(current.getSnap()) != oldLife.contains(current.getSnap())) { + Swing.runIfSwingOrRunLater(() -> updateActionsEnabled()); + } + } + }; + + private final StateEditingModeChangeListener listenerForModeChanges = this::modeChanged; + private final EmulatorStateListener listenerForEmuStateChanges = new EmulatorStateListener() { + @Override + public void running(CachedEmulator emu) { + Swing.runIfSwingOrRunLater(() -> updateActions()); + } + + @Override + public void stopped(CachedEmulator emu) { + Swing.runIfSwingOrRunLater(() -> updateActions()); + } + }; + + protected DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; + + protected MultiStateDockingAction actionEditMode; + + DockingAction actionTargetResume; + DockingAction actionTargetInterrupt; + DockingAction actionTargetKill; + DockingAction actionTargetDisconnect; + DockingAction actionTargetStepInto; + DockingAction actionTargetStepOver; + DockingAction actionTargetStepFinish; + DockingAction actionTargetStepLast; + Set actionsTarget; + + DockingAction actionEmulateResume; + DockingAction actionEmulateInterrupt; + DockingAction actionEmulateStepBack; + DockingAction actionEmulateStepInto; + DockingAction actionEmulateSkipOver; + Set actionsEmulate; + + DockingAction actionTraceSnapBackward; + DockingAction actionTraceSnapForward; + Set actionsTrace; + + Set> actionSets; + Collection curActionSet; + + ActionContext context; + + @AutoServiceConsumed + private DebuggerTraceManagerService traceManager; + // @AutoServiceConsumed // via method + private DebuggerStateEditingService editingService; + // @AutoServiceConsumed // via method + private DebuggerEmulationService emulationService; + + public DebuggerControlPlugin(PluginTool tool) { + super(tool); + + tool.addContextListener(this); + + createActions(); + } + + protected Set getActionSet(StateEditingMode mode) { + switch (mode) { + case READ_ONLY: + case WRITE_TARGET: + return actionsTarget; + case WRITE_TRACE: + return actionsTrace; + case WRITE_EMULATOR: + return actionsEmulate; + default: + throw new AssertionError(); + } + } + + protected Set getActionSet() { + return getActionSet(computeCurrentEditingMode()); + } + + protected void updateActionsEnabled(StateEditingMode mode) { + for (DockingAction action : getActionSet(mode)) { + action.setEnabled(action.isEnabledForContext(context)); + } + } + + protected void updateActionsEnabled() { + updateActionsEnabled(computeCurrentEditingMode()); + } + + @Override + public void contextChanged(ActionContext context) { + this.context = context; + updateActionsEnabled(); + } + + protected void createActions() { + actionEditMode = EditModeAction.builder(this) + .enabled(false) + .enabledWhen(c -> current.getTrace() != null) + .onActionStateChanged(this::activateEditMode) + .buildAndInstall(tool); + + actionTargetResume = TargetResumeAction.builder(this) + .enabledWhenTarget(this::isActionTargetResumeEnabled) + .onTargetAction(this::activatedTargetResume) + .build(); + actionTargetInterrupt = TargetInterruptAction.builder(this) + .enabledWhenTarget(this::isActionTargetInterruptEnabled) + .onTargetAction(this::activatedTargetInterrupt) + .build(); + actionTargetKill = TargetKillAction.builder(this) + .enabledWhenTarget(this::isActionTargetKillEnabled) + .onTargetAction(this::activatedTargetKill) + .build(); + actionTargetDisconnect = DisconnectAction.builder(this) + .enabledWhen(this::isActionTargetDisconnectEnabled) + .onAction(this::activatedTargetDisconnect) + .build(); + actionTargetStepInto = TargetStepIntoAction.builder(this) + .enabledWhenTarget(this::isActionTargetStepEnabled) + .onTargetAction(this::activatedTargetStepInto) + .build(); + actionTargetStepOver = TargetStepOverAction.builder(this) + .enabledWhenTarget(this::isActionTargetStepEnabled) + .onTargetAction(this::activatedTargetStepOver) + .build(); + actionTargetStepFinish = TargetStepFinishAction.builder(this) + .enabledWhenTarget(this::isActionTargetStepEnabled) + .onTargetAction(this::activatedTargetStepFinish) + .build(); + actionTargetStepLast = TargetStepLastAction.builder(this) + .enabledWhenTarget(this::isActionTargetStepEnabled) + .onTargetAction(this::activatedTargetStepLast) + .build(); + actionsTarget = Set.of(actionTargetResume, actionTargetInterrupt, actionTargetKill, + actionTargetDisconnect, actionTargetStepInto, actionTargetStepOver, + actionTargetStepFinish, actionTargetStepLast); + + actionEmulateResume = EmulateResumeAction.builder(this) + .enabledWhen(this::isActionEmulateResumeEnabled) + .onAction(this::activateEmulateResume) + .build(); + actionEmulateInterrupt = EmulateInterruptAction.builder(this) + .enabledWhen(this::isActionEmulateInterruptEnabled) + .onAction(this::activateEmulateInterrupt) + .build(); + actionEmulateStepBack = EmulateStepBackAction.builder(this) + .enabledWhen(this::isActionEmulateStepBackEnabled) + .onAction(this::activateEmulateStepBack) + .build(); + actionEmulateStepInto = EmulateStepIntoAction.builder(this) + .enabledWhen(this::isActionEmulateStepIntoEnabled) + .onAction(this::activateEmulateStepInto) + .build(); + actionEmulateSkipOver = EmulateSkipOverAction.builder(this) + .enabledWhen(this::isActionEmulateSkipOverEnabled) + .onAction(this::activateEmulateSkipOver) + .build(); + actionsEmulate = Set.of(actionEmulateResume, actionEmulateInterrupt, actionEmulateStepBack, + actionEmulateStepInto, actionEmulateSkipOver); + + actionTraceSnapBackward = TraceSnapBackwardAction.builder(this) + .enabledWhen(this::isActionTraceSnapBackwardEnabled) + .onAction(this::activateTraceSnapBackward) + .build(); + actionTraceSnapForward = TraceSnapForwardAction.builder(this) + .enabledWhen(this::isActionTraceSnapForwardEnabled) + .onAction(this::activateTraceSnapForward) + .build(); + actionsTrace = Set.of(actionTraceSnapBackward, actionTraceSnapForward); + + actionSets = Set.of(actionsTarget, actionsEmulate, actionsTrace); + + updateActions(); + } + + protected void activateEditMode(ActionState state, EventTrigger trigger) { + if (current.getTrace() == null) { + return; + } + if (editingService == null) { + return; + } + editingService.setCurrentMode(current.getTrace(), state.getUserData()); + // TODO: Limit selectable modes? + // No sense showing Write Target, if the trace can never be live, again.... + } + + private void modeChanged(Trace trace, StateEditingMode mode) { + Swing.runIfSwingOrRunLater(() -> { + if (current.getTrace() == trace) { + updateActions(); + } + }); + } + + private TargetExecutionState getStateOf(TraceObject traceObject, TargetObject targetObject) { + if (traceObject != null) { + return traceObject.getExecutionState(current.getSnap()); + } + TargetExecutionStateful stateful = + targetObject.getCachedSuitable(TargetExecutionStateful.class); + return stateful == null ? null : stateful.getExecutionState(); + } + + private boolean isActionTargetResumeEnabled(TraceObject object, TargetResumable resumable) { + TargetExecutionState state = getStateOf(object, resumable); + // If the object isn't stateful, always allow this action. Such models should be corrected + return state == null || state.isStopped(); + } + + private CompletableFuture activatedTargetResume(TargetResumable resumable) { + return resumable.resume(); + } + + private boolean isActionTargetInterruptEnabled(TraceObject object, + TargetInterruptible interruptible) { + TargetExecutionState state = getStateOf(object, interruptible); + // If the object isn't stateful, always allow this action. + return state == null || state.isRunning(); + } + + private CompletableFuture activatedTargetInterrupt(TargetInterruptible interruptible) { + return interruptible.interrupt(); + } + + private boolean isActionTargetKillEnabled(TraceObject object, TargetKillable killable) { + TargetExecutionState state = getStateOf(object, killable); + // If the object isn't stateful, always allow this action. Such models should be corrected + return state == null || state.isAlive(); + } + + private CompletableFuture activatedTargetKill(TargetKillable killable) { + return killable.kill(); + } + + private boolean isActionTargetDisconnectEnabled(ActionContext context) { + return current.isAlive(); + } + + private void activatedTargetDisconnect(ActionContext context) { + TraceRecorder recorder = current.getRecorder(); + if (recorder == null) { + return; + } + DebuggerObjectModel model = recorder.getTarget().getModel(); + model.close().exceptionally(ex -> { + tool.setStatusInfo("Disconnect failed: " + ex.getMessage(), true); + Msg.error(this, "Disconnect failed", ex); + return null; + }); + } + + private boolean isActionTargetStepEnabled(TraceObject object, TargetSteppable steppable) { + TargetExecutionState state = getStateOf(object, steppable); + // If the object isn't stateful, always allow this action. Such models should be corrected + return state == null || state.isStopped(); + } + + private CompletableFuture activatedTargetStepInto(TargetSteppable steppable) { + return steppable.step(TargetStepKind.INTO); + } + + private CompletableFuture activatedTargetStepOver(TargetSteppable steppable) { + return steppable.step(TargetStepKind.OVER); + } + + private CompletableFuture activatedTargetStepFinish(TargetSteppable steppable) { + return steppable.step(TargetStepKind.FINISH); + } + + private CompletableFuture activatedTargetStepLast(TargetSteppable steppable) { + return steppable.step(TargetStepKind.EXTENDED); + } + + private boolean haveEmuAndTrace() { + if (emulationService == null) { + return false; + } + if (current.getTrace() == null) { + return false; + } + return true; + } + + private boolean haveEmuAndThread() { + if (emulationService == null) { + return false; + } + if (current.getThread() == null) { + return false; + } + return true; + } + + private DebuggerPcodeMachine getBusyEmulator() { + /** + * NOTE: Could search for current trace, but task manager will only allow one to actually + * run at a time. Best not let the user queue a bunch up if another trace's emulator is + * hogging the manager thread. + */ + for (CachedEmulator ce : emulationService.getBusyEmulators()) { + return ce.emulator(); + } + return null; + } + + private boolean isActionEmulateResumeEnabled(ActionContext context) { + if (!haveEmuAndThread()) { + return false; + } + return getBusyEmulator() == null; + } + + private void activateEmulateResume(ActionContext context) { + if (!haveEmuAndThread()) { + return; + } + if (getBusyEmulator() != null) { + return; + } + DebuggerCoordinates current = this.current; + emulationService.backgroundRun(current.getPlatform(), current.getTime(), + Scheduler.oneThread(current.getThread())).thenAcceptAsync(r -> { + traceManager.activate(current.time(r.schedule())); + }, AsyncUtils.SWING_EXECUTOR).exceptionally(ex -> { + Msg.showError(this, null, "Emulate", "Error emulating", ex); + return null; + }); + } + + private boolean isActionEmulateInterruptEnabled(ActionContext context) { + if (!haveEmuAndThread()) { + return false; + } + return getBusyEmulator() != null; + } + + private void activateEmulateInterrupt(ActionContext context) { + if (emulationService == null) { + return; + } + DebuggerPcodeMachine emu = getBusyEmulator(); + emu.setSuspended(true); + } + + private boolean isActionEmulateStepBackEnabled(ActionContext context) { + if (!haveEmuAndTrace()) { + return false; + } + if (current.getTime().steppedBackward(current.getTrace(), 1) == null) { + return false; + } + return true; + } + + private void activateEmulateStepBack(ActionContext context) { + traceManager.activateTime(current.getTime().steppedBackward(current.getTrace(), 1)); + } + + private boolean isActionEmulateStepIntoEnabled(ActionContext context) { + return haveEmuAndThread(); + } + + private void activateEmulateStepInto(ActionContext context) { + traceManager.activateTime(current.getTime().steppedForward(current.getThread(), 1)); + } + + private boolean isActionEmulateSkipOverEnabled(ActionContext context) { + return haveEmuAndThread(); + } + + private void activateEmulateSkipOver(ActionContext context) { + traceManager.activateTime(current.getTime().skippedForward(current.getThread(), 1)); + } + + private boolean isActionTraceSnapBackwardEnabled(ActionContext context) { + if (current.getTrace() == null) { + return false; + } + if (!current.getTime().isSnapOnly()) { + return true; + } + if (current.getSnap() <= 0) { + return false; + } + return true; + } + + private void activateTraceSnapBackward(ActionContext context) { + if (current.getTime().isSnapOnly()) { + traceManager.activateSnap(current.getSnap() - 1); + } + else { + traceManager.activateSnap(current.getSnap()); + } + } + + private boolean isActionTraceSnapForwardEnabled(ActionContext context) { + Trace curTrace = current.getTrace(); + if (curTrace == null) { + return false; + } + Long maxSnap = curTrace.getTimeManager().getMaxSnap(); + if (maxSnap == null || current.getSnap() >= maxSnap) { + return false; + } + return true; + } + + private void activateTraceSnapForward(ActionContext contetxt) { + traceManager.activateSnap(current.getSnap() + 1); + } + + protected void coordinatesActivated(DebuggerCoordinates coords) { + if (current.getTrace() != coords.getTrace()) { + if (current.getTrace() != null) { + current.getTrace().removeListener(listenerForObjects); + } + if (coords.getTrace() != null) { + coords.getTrace().addListener(listenerForObjects); + } + } + current = coords; + updateActions(); + } + + private StateEditingMode computeCurrentEditingMode() { + // TODO: We're sort of piggy-backing our mode onto that of the editing service. + // Seems we should have our own? + if (editingService == null) { + return StateEditingMode.READ_ONLY; + } + if (current.getTrace() == null) { + return StateEditingMode.READ_ONLY; + } + return editingService.getCurrentMode(current.getTrace()); + } + + private void hideActions(Collection actions) { + if (tool == null) { + return; + } + if (curActionSet == actions) { + curActionSet = null; + } + for (DockingActionIf action : actions) { + tool.removeAction(action); + } + } + + private void showActions(Collection actions) { + if (tool == null) { + return; + } + if (curActionSet == actions) { + return; + } + for (DockingActionIf action : actions) { + tool.addAction(action); + } + curActionSet = actions; + } + + private void updateActions() { + StateEditingMode mode = computeCurrentEditingMode(); + actionEditMode.setCurrentActionStateByUserData(mode); + + Set actions = getActionSet(mode); + for (Set set : actionSets) { + if (set == actions) { + showActions(set); + } + else { + hideActions(set); + } + } + updateActionsEnabled(mode); + } + + protected void traceClosed(Trace trace) { + if (current.getTrace() == trace) { + trace.removeListener(listenerForObjects); + current = DebuggerCoordinates.NOWHERE; + } + updateActions(); + } + + @AutoServiceConsumed + protected void setEditingService(DebuggerStateEditingService editingService) { + if (this.editingService != null) { + this.editingService.removeModeChangeListener(listenerForModeChanges); + } + this.editingService = editingService; + if (this.editingService != null) { + this.editingService.addModeChangeListener(listenerForModeChanges); + } + updateActions(); + } + + @AutoServiceConsumed + protected void setEmulationService(DebuggerEmulationService emulationService) { + if (this.emulationService != null) { + this.emulationService.removeStateListener(listenerForEmuStateChanges); + } + this.emulationService = emulationService; + if (this.emulationService != null) { + this.emulationService.addStateListener(listenerForEmuStateChanges); + } + updateActions(); + } + + @Override + public void processEvent(PluginEvent event) { + super.processEvent(event); + if (event instanceof TraceActivatedPluginEvent ev) { + coordinatesActivated(ev.getActiveCoordinates()); + } + else if (event instanceof TraceClosedPluginEvent ev) { + traceClosed(ev.getTrace()); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/editing/DebuggerStateEditingPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/editing/DebuggerStateEditingPlugin.java deleted file mode 100644 index 5a3a0e732f..0000000000 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/editing/DebuggerStateEditingPlugin.java +++ /dev/null @@ -1,142 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.plugin.core.debug.gui.editing; - -import docking.menu.ActionState; -import docking.menu.MultiStateDockingAction; -import docking.widgets.EventTrigger; -import ghidra.app.plugin.PluginCategoryNames; -import ghidra.app.plugin.core.debug.*; -import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent; -import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; -import ghidra.app.plugin.core.debug.gui.DebuggerResources.EditModeAction; -import ghidra.app.services.DebuggerStateEditingService; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; -import ghidra.app.services.DebuggerStateEditingService.StateEditingModeChangeListener; -import ghidra.framework.plugintool.*; -import ghidra.framework.plugintool.annotation.AutoServiceConsumed; -import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.trace.model.Trace; - -@PluginInfo( - shortDescription = "Debugger machine-state Editing GUI", - description = "GUI to edit target, trace, and/or emulation machine state", - category = PluginCategoryNames.DEBUGGER, - packageName = DebuggerPluginPackage.NAME, - status = PluginStatus.RELEASED, - eventsConsumed = { - TraceActivatedPluginEvent.class, - TraceClosedPluginEvent.class, - }, - servicesRequired = { - DebuggerStateEditingService.class, - }) -public class DebuggerStateEditingPlugin extends AbstractDebuggerPlugin { - - private final StateEditingModeChangeListener listenerForModeChanges = this::modeChanged; - - protected DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; - - protected MultiStateDockingAction actionEditMode; - - // @AutoServiceConsumed // via method - private DebuggerStateEditingService editingService; - - public DebuggerStateEditingPlugin(PluginTool tool) { - super(tool); - - createActions(); - } - - protected void createActions() { - actionEditMode = EditModeAction.builder(this) - .enabled(false) - .enabledWhen(c -> current.getTrace() != null) - .onActionStateChanged(this::activateEditMode) - .buildAndInstall(tool); - } - - protected void activateEditMode(ActionState state, EventTrigger trigger) { - if (current.getTrace() == null) { - return; - } - if (editingService == null) { - return; - } - editingService.setCurrentMode(current.getTrace(), state.getUserData()); - // TODO: Limit selectable modes? - // No sense showing Write Target, if the trace can never be live, again.... - } - - private void modeChanged(Trace trace, StateEditingMode mode) { - if (current.getTrace() == trace) { - refreshActionMode(); - } - } - - protected void coordinatesActivated(DebuggerCoordinates coords) { - current = coords; - refreshActionMode(); - // tool.contextChanged(null); - } - - private StateEditingMode computeCurrentEditingMode() { - if (editingService == null) { - return StateEditingMode.READ_ONLY; - } - if (current.getTrace() == null) { - return StateEditingMode.READ_ONLY; - } - return editingService.getCurrentMode(current.getTrace()); - } - - private void refreshActionMode() { - actionEditMode.setCurrentActionStateByUserData(computeCurrentEditingMode()); - } - - protected void traceClosed(Trace trace) { - if (current.getTrace() == trace) { - current = DebuggerCoordinates.NOWHERE; - } - refreshActionMode(); - // tool.contextChanged(null); - } - - @AutoServiceConsumed - protected void setEditingService(DebuggerStateEditingService editingService) { - if (this.editingService != null) { - this.editingService.removeModeChangeListener(listenerForModeChanges); - } - this.editingService = editingService; - if (this.editingService != null) { - this.editingService.addModeChangeListener(listenerForModeChanges); - } - refreshActionMode(); - } - - @Override - public void processEvent(PluginEvent event) { - super.processEvent(event); - if (event instanceof TraceActivatedPluginEvent) { - TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; - coordinatesActivated(ev.getActiveCoordinates()); - } - else if (event instanceof TraceClosedPluginEvent) { - TraceClosedPluginEvent ev = (TraceClosedPluginEvent) event; - traceClosed(ev.getTrace()); - } - } -} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java index 335a1ad4c7..e800449343 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServicePlugin.java @@ -43,19 +43,22 @@ import ghidra.async.AsyncLazyMap; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.program.model.address.Address; +import ghidra.pcode.emu.PcodeMachine.AccessKind; +import ghidra.program.model.address.*; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; import ghidra.trace.model.*; +import ghidra.trace.model.breakpoint.*; import ghidra.trace.model.guest.TracePlatform; import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.time.TraceSnapshot; -import ghidra.trace.model.time.schedule.CompareResult; -import ghidra.trace.model.time.schedule.TraceSchedule; +import ghidra.trace.model.time.schedule.*; +import ghidra.trace.model.time.schedule.Scheduler.RunResult; import ghidra.util.Msg; import ghidra.util.classfinder.ClassSearcher; import ghidra.util.database.UndoableTransaction; +import ghidra.util.datastruct.ListenerSet; import ghidra.util.exception.CancelledException; import ghidra.util.task.Task; import ghidra.util.task.TaskMonitor; @@ -139,27 +142,19 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm } } - protected static class CachedEmulator { - final DebuggerPcodeMachine emulator; + protected abstract class AbstractEmulateTask extends Task { + protected final CompletableFuture future = new CompletableFuture<>(); - public CachedEmulator(DebuggerPcodeMachine emulator) { - this.emulator = emulator; + public AbstractEmulateTask(String title, boolean hasProgress) { + super(title, true, hasProgress, false, false); } - } - protected class EmulateTask extends Task { - protected final CacheKey key; - protected final CompletableFuture future = new CompletableFuture<>(); - - public EmulateTask(CacheKey key) { - super("Emulate " + key.time + " in " + key.trace, true, true, false, false); - this.key = key; - } + protected abstract T compute(TaskMonitor monitor) throws CancelledException; @Override public void run(TaskMonitor monitor) throws CancelledException { try { - future.complete(doEmulate(key, monitor)); + future.complete(compute(monitor)); } catch (CancelledException e) { future.completeExceptionally(e); @@ -172,6 +167,36 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm } } + protected class EmulateTask extends AbstractEmulateTask { + protected final CacheKey key; + + public EmulateTask(CacheKey key) { + super("Emulate " + key.time + " in " + key.trace, true); + this.key = key; + } + + @Override + protected Long compute(TaskMonitor monitor) throws CancelledException { + return doEmulate(key, monitor); + } + } + + protected class RunEmulatorTask extends AbstractEmulateTask { + private final CacheKey from; + private final Scheduler scheduler; + + public RunEmulatorTask(CacheKey from, Scheduler scheduler) { + super("Emulating...", false); + this.from = from; + this.scheduler = scheduler; + } + + @Override + protected EmulationResult compute(TaskMonitor monitor) throws CancelledException { + return doRun(from, monitor, scheduler); + } + } + protected DebuggerPcodeEmulatorFactory emulatorFactory = new BytesDebuggerPcodeEmulatorFactory(); @@ -181,6 +206,53 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm new AsyncLazyMap<>(new HashMap<>(), this::doBackgroundEmulate) .forgetErrors((key, t) -> true) .forgetValues((key, l) -> true); + protected final Map busy = new LinkedHashMap<>(); + protected final ListenerSet stateListeners = + new ListenerSet<>(EmulatorStateListener.class); + + class BusyEmu implements AutoCloseable { + private final CachedEmulator ce; + + private BusyEmu(CachedEmulator ce) { + this.ce = ce; + boolean fire = false; + synchronized (busy) { + Integer count = busy.get(ce); + if (count == null) { + busy.put(ce, 1); + fire = true; + } + else { + busy.put(ce, count + 1); + } + } + if (fire) { + stateListeners.fire.running(ce); + } + } + + @Override + public void close() { + boolean fire = false; + synchronized (busy) { + int count = busy.get(ce); + if (count == 1) { + busy.remove(ce); + fire = true; + } + else { + busy.put(ce, count - 1); + } + } + if (fire) { + stateListeners.fire.stopped(ce); + } + } + + public BusyEmu dup() { + return new BusyEmu(ce); + } + } @AutoServiceConsumed private DebuggerTraceManagerService traceManager; @@ -429,17 +501,23 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm @Override public CompletableFuture backgroundEmulate(TracePlatform platform, TraceSchedule time) { - Trace trace = platform.getTrace(); - if (!traceManager.getOpenTraces().contains(trace)) { - throw new IllegalArgumentException( - "Cannot emulate a trace unless it's opened in the tool."); - } + requireOpen(platform.getTrace()); if (time.isSnapOnly()) { return CompletableFuture.completedFuture(time.getSnap()); } return requests.get(new CacheKey(platform, time)); } + @Override + public CompletableFuture backgroundRun(TracePlatform platform, + TraceSchedule from, Scheduler scheduler) { + requireOpen(platform.getTrace()); + CacheKey key = new CacheKey(platform, from); + RunEmulatorTask task = new RunEmulatorTask(key, scheduler); + tool.execute(task, 500); + return task.future; + } + protected TraceSnapshot findScratch(Trace trace, TraceSchedule time) { Collection exist = trace.getTimeManager().getSnapshotsWithSchedule(time); @@ -461,13 +539,43 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm return snapshot; } - protected long doEmulate(CacheKey key, TaskMonitor monitor) throws CancelledException { + protected void installBreakpoints(Trace trace, long snap, DebuggerPcodeMachine emu) { + Range span = Range.singleton(snap); + TraceBreakpointManager bm = trace.getBreakpointManager(); + for (AddressSpace as : trace.getBaseAddressFactory().getAddressSpaces()) { + for (TraceBreakpoint bpt : bm.getBreakpointsIntersecting(span, + new AddressRangeImpl(as.getMinAddress(), as.getMaxAddress()))) { + if (!bpt.isEnabled(snap)) { + continue; + } + Set kinds = bpt.getKinds(); + boolean isExecute = + kinds.contains(TraceBreakpointKind.HW_EXECUTE) || + kinds.contains(TraceBreakpointKind.SW_EXECUTE); + boolean isRead = kinds.contains(TraceBreakpointKind.READ); + boolean isWrite = kinds.contains(TraceBreakpointKind.WRITE); + if (isExecute) { + emu.addBreakpoint(bpt.getMinAddress(), "1:1"); + } + if (isRead && isWrite) { + emu.addAccessBreakpoint(bpt.getRange(), AccessKind.RW); + } + else if (isRead) { + emu.addAccessBreakpoint(bpt.getRange(), AccessKind.R); + } + else if (isWrite) { + emu.addAccessBreakpoint(bpt.getRange(), AccessKind.W); + } + } + } + } + + protected BusyEmu doEmulateFromCached(CacheKey key, TaskMonitor monitor) + throws CancelledException { Trace trace = key.trace; TracePlatform platform = key.platform; TraceSchedule time = key.time; - CachedEmulator ce; - DebuggerPcodeMachine emu; Map.Entry ancestor = findNearestPrefix(key); if (ancestor != null) { CacheKey prevKey = ancestor.getKey(); @@ -479,28 +587,32 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm // TODO: Handle errors, and add to proper place in cache? // TODO: Finish partially-executed instructions? - ce = ancestor.getValue(); - emu = ce.emulator; - monitor.initialize(time.totalTickCount() - prevKey.time.totalTickCount()); - createRegisterSpaces(trace, time, monitor); - monitor.setMessage("Emulating"); - time.finish(trace, prevKey.time, emu, monitor); + try (BusyEmu be = new BusyEmu(ancestor.getValue())) { + DebuggerPcodeMachine emu = be.ce.emulator(); + + emu.clearAllInjects(); + emu.clearAccessBreakpoints(); + emu.setSuspended(false); + + monitor.initialize(time.totalTickCount() - prevKey.time.totalTickCount()); + createRegisterSpaces(trace, time, monitor); + monitor.setMessage("Emulating"); + time.finish(trace, prevKey.time, emu, monitor); + return be.dup(); + } } - else { - emu = emulatorFactory.create(tool, platform, time.getSnap(), - modelService == null ? null : modelService.getRecorder(trace)); - ce = new CachedEmulator(emu); + DebuggerPcodeMachine emu = emulatorFactory.create(tool, platform, time.getSnap(), + modelService == null ? null : modelService.getRecorder(trace)); + try (BusyEmu be = new BusyEmu(new CachedEmulator(key.trace, emu))) { monitor.initialize(time.totalTickCount()); createRegisterSpaces(trace, time, monitor); monitor.setMessage("Emulating"); time.execute(trace, emu, monitor); + return be.dup(); } - TraceSnapshot destSnap; - try (UndoableTransaction tid = UndoableTransaction.start(trace, "Emulate")) { - destSnap = findScratch(trace, time); - emu.writeDown(platform, destSnap.getKey(), time.getSnap()); - } + } + protected void cacheEmulator(CacheKey key, CachedEmulator ce) { synchronized (cache) { cache.put(key, ce); eldest.add(key); @@ -511,8 +623,35 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm cache.remove(expired); } } + } - return destSnap.getKey(); + protected TraceSnapshot writeToScratch(CacheKey key, CachedEmulator ce) { + try (UndoableTransaction tid = UndoableTransaction.start(key.trace, "Emulate")) { + TraceSnapshot destSnap = findScratch(key.trace, key.time); + ce.emulator().writeDown(key.platform, destSnap.getKey(), key.time.getSnap()); + return destSnap; + } + } + + protected long doEmulate(CacheKey key, TaskMonitor monitor) throws CancelledException { + try (BusyEmu be = doEmulateFromCached(key, monitor)) { + TraceSnapshot destSnap = writeToScratch(key, be.ce); + cacheEmulator(key, be.ce); + return destSnap.getKey(); + } + } + + protected EmulationResult doRun(CacheKey key, TaskMonitor monitor, Scheduler scheduler) + throws CancelledException { + try (BusyEmu be = doEmulateFromCached(key, monitor)) { + installBreakpoints(key.trace, key.time.getSnap(), be.ce.emulator()); + TraceThread eventThread = key.time.getEventThread(key.trace); + RunResult result = scheduler.run(key.trace, eventThread, be.ce.emulator(), monitor); + key = new CacheKey(key.platform, key.time.advanced(result.schedule())); + TraceSnapshot destSnap = writeToScratch(key, be.ce); + cacheEmulator(key, be.ce); + return new RecordEmulationResult(key.time, destSnap.getKey(), result.error()); + } } protected void createRegisterSpaces(Trace trace, TraceSchedule time, TaskMonitor monitor) { @@ -529,25 +668,53 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm } } - @Override - public long emulate(TracePlatform platform, TraceSchedule time, TaskMonitor monitor) - throws CancelledException { - Trace trace = platform.getTrace(); + protected void requireOpen(Trace trace) { if (!traceManager.getOpenTraces().contains(trace)) { throw new IllegalArgumentException( "Cannot emulate a trace unless it's opened in the tool."); } + } + + @Override + public long emulate(TracePlatform platform, TraceSchedule time, TaskMonitor monitor) + throws CancelledException { + requireOpen(platform.getTrace()); if (time.isSnapOnly()) { return time.getSnap(); } return doEmulate(new CacheKey(platform, time), monitor); } + @Override + public EmulationResult run(TracePlatform platform, TraceSchedule from, TaskMonitor monitor, + Scheduler scheduler) throws CancelledException { + Trace trace = platform.getTrace(); + requireOpen(trace); + return doRun(new CacheKey(platform, from), monitor, scheduler); + } + @Override public DebuggerPcodeMachine getCachedEmulator(Trace trace, TraceSchedule time) { CachedEmulator ce = cache.get(new CacheKey(trace.getPlatformManager().getHostPlatform(), time)); - return ce == null ? null : ce.emulator; + return ce == null ? null : ce.emulator(); + } + + @Override + public Collection getBusyEmulators() { + synchronized (busy) { + return List.copyOf(busy.keySet()); + } + } + + @Override + public void addStateListener(EmulatorStateListener listener) { + stateListeners.add(listener); + } + + @Override + public void removeStateListener(EmulatorStateListener listener) { + stateListeners.remove(listener); } @AutoServiceConsumed diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java index 326803058a..c22aaebe6d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java @@ -523,7 +523,6 @@ public class DebuggerModelServicePlugin extends Plugin throws IOException { String traceName = nameTrace(target); Trace trace = new DBTrace(traceName, mapper.getTraceCompilerSpec(), this); - //DefaultTraceRecorder recorder = new DefaultTraceRecorder(this, trace, target, mapper); TraceRecorder recorder = mapper.startRecording(this, trace); trace.release(this); // The recorder now owns it (on behalf of the service) return recorder; 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 c4ffc0e812..0332d4a762 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 @@ -47,6 +47,7 @@ import ghidra.trace.model.memory.TraceMemoryRegion; import ghidra.trace.model.modules.TraceModule; import ghidra.trace.model.modules.TraceSection; import ghidra.trace.model.stack.TraceStackFrame; +import ghidra.trace.model.target.TraceObject; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.time.TraceSnapshot; import ghidra.util.Msg; @@ -107,6 +108,16 @@ public class DefaultTraceRecorder implements TraceRecorder { /*---------------- OBJECT MANAGER METHODS -------------------*/ + @Override + public TargetObject getTargetObject(TraceObject obj) { + return null; + } + + @Override + public TraceObject getTraceObject(TargetObject obj) { + return null; + } + @Override public TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt) { return objectManager.getTargetBreakpoint(bpt); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorder.java index 15a5beef8f..b80fc4febc 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorder.java @@ -351,6 +351,16 @@ public class ObjectBasedTraceRecorder implements TraceRecorder { listeners.remove(listener); } + @Override + public TargetObject getTargetObject(TraceObject obj) { + return objectRecorder.toTarget(obj); + } + + @Override + public TraceObject getTraceObject(TargetObject obj) { + return objectRecorder.toTrace(obj); + } + @Override public TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt) { return objectRecorder.getTargetInterface(bpt, TraceObjectBreakpointLocation.class, diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java index a01840db2a..aa29c13edb 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java @@ -19,8 +19,7 @@ import java.io.IOException; import java.lang.invoke.MethodHandles; import java.net.ConnectException; import java.util.*; -import java.util.concurrent.*; -import java.util.function.Supplier; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import docking.ActionContext; @@ -38,6 +37,7 @@ import ghidra.async.AsyncConfigFieldCodec.BooleanAsyncConfigFieldCodec; import ghidra.dbg.target.*; import ghidra.framework.client.ClientUtil; import ghidra.framework.client.NotConnectedException; +import ghidra.framework.data.DomainObjectAdapterDB; import ghidra.framework.main.DataTreeDialog; import ghidra.framework.model.*; import ghidra.framework.options.SaveState; @@ -131,6 +131,42 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } } + static class TransactionEndFuture extends CompletableFuture + implements TransactionListener { + final Trace trace; + + public TransactionEndFuture(Trace trace) { + this.trace = trace; + this.trace.addTransactionListener(this); + if (this.trace.getCurrentTransaction() == null) { + complete(null); + } + } + + @Override + public void transactionStarted(DomainObjectAdapterDB domainObj, Transaction tx) { + } + + @Override + public boolean complete(Void value) { + trace.removeTransactionListener(this); + return super.complete(value); + } + + @Override + public void transactionEnded(DomainObjectAdapterDB domainObj) { + complete(null); + } + + @Override + public void undoStackChanged(DomainObjectAdapterDB domainObj) { + } + + @Override + public void undoRedoOccurred(DomainObjectAdapterDB domainObj) { + } + } + // TODO: This is a bit out of this manager's bounds, but acceptable for now. class ForRecordersListener implements CollectionChangeListener { @Override @@ -138,9 +174,25 @@ public class DebuggerTraceManagerServicePlugin extends Plugin Swing.runLater(() -> updateCurrentRecorder()); } + public CompletableFuture waitUnlockedDebounced(TraceRecorder recorder) { + Trace trace = recorder.getTrace(); + return new TransactionEndFuture(trace) + .thenCompose(__ -> AsyncTimer.DEFAULT_TIMER.mark().after(100)) + .thenComposeAsync(__ -> { + if (trace.isLocked()) { + return waitUnlockedDebounced(recorder); + } + return AsyncUtils.NIL; + }); + } + @Override public void elementRemoved(TraceRecorder recorder) { - Swing.runLater(() -> { + boolean save = isSaveTracesByDefault(); + CompletableFuture flush = save + ? waitUnlockedDebounced(recorder) + : AsyncUtils.NIL; + flush.thenRunAsync(() -> { updateCurrentRecorder(); if (!isAutoCloseOnTerminate()) { return; @@ -151,13 +203,12 @@ public class DebuggerTraceManagerServicePlugin extends Plugin return; } } - if (!isSaveTracesByDefault()) { - closeTrace(trace); - return; + if (save) { + // Errors already handled by saveTrace + saveTrace(trace); } - // Errors already handled by saveTrace - tryHarder(() -> saveTrace(trace), 3, 100).thenRun(() -> closeTrace(trace)); - }); + closeTrace(trace); + }, AsyncUtils.SWING_EXECUTOR); } } @@ -208,22 +259,6 @@ public class DebuggerTraceManagerServicePlugin extends Plugin return t; } - protected CompletableFuture tryHarder(Supplier> action, int retries, - long retryAfterMillis) { - Executor exe = CompletableFuture.delayedExecutor(retryAfterMillis, TimeUnit.MILLISECONDS); - // NB. thenCompose(f -> f) also ensures exceptions are handled here, not passed through - CompletableFuture result = - CompletableFuture.supplyAsync(action, AsyncUtils.SWING_EXECUTOR).thenCompose(f -> f); - if (retries > 0) { - return result.thenApply(CompletableFuture::completedFuture).exceptionally(ex -> { - return CompletableFuture - .supplyAsync(() -> tryHarder(action, retries - 1, retryAfterMillis), exe) - .thenCompose(f -> f); - }).thenCompose(f -> f); - } - return result; - } - @Override protected void init() { super.init(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerEmulationService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerEmulationService.java index bdb7b2c785..249d8fb16d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerEmulationService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerEmulationService.java @@ -22,6 +22,8 @@ import ghidra.app.plugin.core.debug.service.emulation.*; import ghidra.framework.plugintool.ServiceInfo; import ghidra.trace.model.Trace; import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.time.schedule.Scheduler; +import ghidra.trace.model.time.schedule.Scheduler.RunResult; import ghidra.trace.model.time.schedule.TraceSchedule; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -37,6 +39,71 @@ import ghidra.util.task.TaskMonitor; @ServiceInfo(defaultProvider = DebuggerEmulationServicePlugin.class) public interface DebuggerEmulationService { + interface EmulationResult extends RunResult { + /** + * Get the (scratch) snapshot where the emulated state is stored + * + * @return the snapshot + */ + public long snapshot(); + } + + /** + * The result of letting the emulator "run free" + */ + record RecordEmulationResult(TraceSchedule schedule, long snapshot, Throwable error) + implements EmulationResult { + } + + /** + * An emulator managed by this service + */ + record CachedEmulator(Trace trace, DebuggerPcodeMachine emulator) { + /** + * Get the trace to which the emulator is bound + * + * @return the trace + */ + @Override + public Trace trace() { + return trace; + } + + /** + * Get the emulator + * + *

    + * WARNING: This emulator belongs to this service. You may interrupt it, but stepping + * it, or otherwise manipulating it without the service's knowledge can lead to unintended + * consequences. + * + * @return the emulator + */ + @Override + public DebuggerPcodeMachine emulator() { + return emulator; + } + } + + /** + * A listener for changes in emulator state + */ + interface EmulatorStateListener { + /** + * An emulator is running + * + * @param emu the emulator + */ + void running(CachedEmulator emu); + + /** + * An emulator has stopped + * + * @param emu the emulator + */ + void stopped(CachedEmulator emu); + } + /** * Get the available emulator factories * @@ -53,7 +120,7 @@ public interface DebuggerEmulationService { * the tool, but the config options for each factory to the program/trace. * *

    - * TODO: Should there be some opinion service for choosing default configs? Seem overly + * TODO: Should there be some opinion service for choosing default configs? Seems overly * complicated for what it offers. For now, we won't save anything, we'll default to the * (built-in) {@link BytesDebuggerPcodeEmulatorFactory}, and we won't have configuration * options. @@ -97,35 +164,74 @@ public interface DebuggerEmulationService { * Emulate using the trace's "host" platform * * @see #emulate(TracePlatform, TraceSchedule, TaskMonitor) - * @param trace - * @param time - * @param monitor - * @return - * @throws CancelledException + * @param trace the trace containing the initial state + * @param time the time coordinates, including initial snap, steps, and p-code steps + * @param monitor a monitor for cancellation and progress reporting + * @return the snap in the trace's scratch space where the realize state is stored + * @throws CancelledException if the emulation is cancelled */ default long emulate(Trace trace, TraceSchedule time, TaskMonitor monitor) throws CancelledException { return emulate(trace.getPlatformManager().getHostPlatform(), time, monitor); } + /** + * Allow the emulator to "run free" until it is interrupted or encounters an error + * + *

    + * The service may perform some preliminary emulation to realize the machine's initial state. If + * the monitor cancels during preliminary emulation, this method throws a + * {@link CancelledException}. If the monitor cancels the emulation during the run, it is + * treated the same as interruption. The machine state will be written to the trace in a scratch + * snap and the result returned. Note that the machine could be interrupted having only + * partially executed an instruction. Thus, the schedule may specify p-code operations. The + * schedule will place the program counter on the instruction (or p-code op) causing the + * interruption. Thus, except for breakpoints, attempting to step again will interrupt the + * emulator again. + * + * @param platform the trace platform containing the initial state + * @param from a schedule for the machine's initial state + * @param monitor a monitor cancellation + * @param scheduler a thread scheduler for the emulator + * @return the result of emulation + */ + EmulationResult run(TracePlatform platform, TraceSchedule from, TaskMonitor monitor, + Scheduler scheduler) throws CancelledException; + /** * Invoke {@link #emulate(Trace, TraceSchedule, TaskMonitor)} in the background * *

    - * This is the preferred means of performing emulation. Because the underlying emulator may - * request a blocking read from a target, it is important that - * {@link #emulate(TracePlatform, TraceSchedule, TaskMonitor)} is never called by the - * Swing thread. + * This is the preferred means of performing definite emulation. Because the underlying emulator + * may request a blocking read from a target, it is important that + * {@link #emulate(TracePlatform, TraceSchedule, TaskMonitor) emulate} is never called + * by the Swing thread. * * @param platform the trace platform containing the initial state * @param time the time coordinates, including initial snap, steps, and p-code steps * @return a future which completes with the result of - * {@link #emulate(TracePlatform, TraceSchedule, TaskMonitor)} + * {@link #emulate(TracePlatform, TraceSchedule, TaskMonitor) emulate} */ CompletableFuture backgroundEmulate(TracePlatform platform, TraceSchedule time); /** - * The the cached emulator for the given trace and time + * Invoke {@link #run(TracePlatform, TraceSchedule, TaskMonitor, Scheduler)} in the background + * + *

    + * This is the preferred means of performing indefinite emulation, for the same reasons as + * {@link #backgroundEmulate(TracePlatform, TraceSchedule) emulate}. + * + * @param platform the trace platform containing the initial state + * @param from a schedule for the machine's initial state + * @param scheduler a thread scheduler for the emulator + * @return a future which completes with the result of + * {@link #run(TracePlatform, TraceSchedule, TaskMonitor, Scheduler) run}. + */ + CompletableFuture backgroundRun(TracePlatform platform, TraceSchedule from, + Scheduler scheduler); + + /** + * Get the cached emulator for the given trace and time * *

    * To guarantee the emulator is present, call {@link #backgroundEmulate(Trace, TraceSchedule)} @@ -142,4 +248,25 @@ public interface DebuggerEmulationService { * @return the copied p-code frame */ DebuggerPcodeMachine getCachedEmulator(Trace trace, TraceSchedule time); + + /** + * Get the emulators which are current executing + * + * @return the collection + */ + Collection getBusyEmulators(); + + /** + * Add a listener for emulator state changes + * + * @param listener the listener + */ + void addStateListener(EmulatorStateListener listener); + + /** + * Remove a listener for emulator state changes + * + * @param listener the listener + */ + void removeStateListener(EmulatorStateListener listener); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java index beedd916e9..153b411884 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java @@ -27,7 +27,8 @@ import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.pcode.utils.Utils; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSetView; -import ghidra.program.model.lang.*; +import ghidra.program.model.lang.Register; +import ghidra.program.model.lang.RegisterValue; import ghidra.trace.model.Trace; import ghidra.trace.model.breakpoint.TraceBreakpoint; import ghidra.trace.model.breakpoint.TraceBreakpointKind; @@ -37,6 +38,7 @@ import ghidra.trace.model.memory.TraceMemorySpace; import ghidra.trace.model.modules.TraceModule; import ghidra.trace.model.modules.TraceSection; import ghidra.trace.model.stack.TraceStackFrame; +import ghidra.trace.model.target.TraceObject; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.time.TraceSnapshot; import ghidra.trace.model.time.TraceTimeManager; @@ -50,7 +52,17 @@ import ghidra.util.task.TaskMonitor; * The recorder is the glue from a portion of a debugger's model into a Ghidra trace. As such, this * object maintains a mapping between corresponding objects of interest in the model tree to the * trace, and that mapping can be queried. In most cases, UI components which deal with tracing need - * only read the trace in order to populate their display. + * only read the trace in order to populate their display. Several methods are provided for + * retrieving corresponding objects from the target or trace given that object in the other. These + * methods may return null for a variety of reasons: + * + *

      + *
    1. The particular type may not be supported or of interest to the recorder.
    2. + *
    3. The recorder may not have actually recorded the object yet, despite receiving notice. + * Recording is asynchronous, and it may also be waiting for additional dependencies or attributes + * before it can create the corresponding trace object.
    4. + *
    5. The target object may not longer exist for a given trace object.
    6. + *
    * *

    * The recorder copies information in one direction; thus, if a trace UI component needs to affect @@ -205,48 +217,210 @@ public interface TraceRecorder { */ void removeListener(TraceRecorderListener listener); + /** + * Get the target object corresponding to the given trace object + * + * @param obj the trace object + * @return the target object, or null + */ + TargetObject getTargetObject(TraceObject obj); + + /** + * Get the trace object corresponding to the given target object + * + * @param obj the target object + * @return the trace object, or null + */ + TraceObject getTraceObject(TargetObject obj); + + /** + * Get the target breakpoint location corresponding to the given trace breakpoint + * + * @param obj the trace breakpoint + * @return the target breakpoint location, or null + */ TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt); + /** + * Get the trace breakpoint corresponding to the given target breakpoint location + * + * @param obj the target breakpoint location + * @return the trace breakpoint, or null + */ TraceBreakpoint getTraceBreakpoint(TargetBreakpointLocation bpt); + /** + * Get the target memory region corresponding to the given trace memory region + * + * @param obj the trace memory region + * @return the target memory region, or null + */ TargetMemoryRegion getTargetMemoryRegion(TraceMemoryRegion region); + /** + * Get the trace memory region corresponding to the given target memory region + * + * @param obj the target memory region + * @return the trace memory region, or null + */ TraceMemoryRegion getTraceMemoryRegion(TargetMemoryRegion region); + /** + * Get the target module corresponding to the given trace module + * + * @param obj the trace module + * @return the target module, or null + */ TargetModule getTargetModule(TraceModule module); + /** + * Get the trace module corresponding to the given target module + * + * @param obj the target module + * @return the trace module, or null + */ TraceModule getTraceModule(TargetModule module); + /** + * Get the target section corresponding to the given trace section + * + * @param obj the trace section + * @return the target section, or null + */ TargetSection getTargetSection(TraceSection section); + /** + * Get the trace section corresponding to the given target section + * + * @param obj the target section + * @return the trace section, or null + */ TraceSection getTraceSection(TargetSection section); + /** + * Get the target thread corresponding to the given trace thread + * + * @param obj the trace thread + * @return the target thread, or null + */ TargetThread getTargetThread(TraceThread thread); + /** + * Get the execution state of the given target thread + * + * @param thread the target thread + * @return the execution state, or null + */ TargetExecutionState getTargetThreadState(TargetThread thread); + /** + * Get the execution state of the given trace thread + * + * @param thread the trace thread + * @return the execution state, or null + */ TargetExecutionState getTargetThreadState(TraceThread thread); + /** + * Get the target register bank for the given trace thread and frame level + * + *

    + * If the model doesn't provide a bank for every frame, then this should only return non-null + * for frame level 0, in which case it should return the bank for the given thread. + * + * @param thread the thread + * @param frameLevel the frame level + * @return the bank, or null + */ TargetRegisterBank getTargetRegisterBank(TraceThread thread, int frameLevel); + /** + * Get the trace thread corresponding to the given target thread + * + * @param obj the target thread + * @return the trace thread, or null + */ TraceThread getTraceThread(TargetThread thread); + /** + * Find the trace thread containing the given successor target object + * + * @param successor the target object + * @return the trace thread containing the object, or null + */ TraceThread getTraceThreadForSuccessor(TargetObject successor); + /** + * Get the trace stack frame for the given target stack frame + * + * @param frame the target stack frame + * @return the trace stack frame, or null + */ TraceStackFrame getTraceStackFrame(TargetStackFrame frame); + /** + * Get the trace stack frame containing the given successor target object + * + * @param successor the target object + * @return the trace stack frame containing the object, or null + */ TraceStackFrame getTraceStackFrameForSuccessor(TargetObject successor); + /** + * Get the target stack frame for the given trace thread and frame level + * + * @param thread the thread + * @param frameLevel the frame level + * @return the stack frame, or null + */ TargetStackFrame getTargetStackFrame(TraceThread thread, int frameLevel); + /** + * Get all the target's threads that are currently alive + * + * @return the set of live target threads + */ Set getLiveTargetThreads(); + /** + * Get the register mapper for the given trace thread + * + * @param thread the trace thread + * @return the mapper, or null + */ DebuggerRegisterMapper getRegisterMapper(TraceThread thread); + /** + * Get the memory mapper for the target + * + * @return the mapper, or null + */ DebuggerMemoryMapper getMemoryMapper(); + /** + * Check if the given register bank is accessible + * + * @param bank the target register bank + * @return true if accessible + * @deprecated the accessibility concept was never really implemented nor offered anything of + * value. It has no replacement. Instead a model should reject requests its not + * prepared to handle, or queue them up to be processed when it can. If the latter, + * then ideally it should only allow one instance of a given request to be queued. + */ + @Deprecated boolean isRegisterBankAccessible(TargetRegisterBank bank); + /** + * Check if the register bank for the given trace thread and frame level is accessible + * + * @param thread the trace thread + * @param frameLevel the frame level + * @see #getTargetStackFrame(TraceThread, int) + * @see #isRegisterBankAccessible(TargetRegisterBank) + * @return true if accessible + * @deprecated for the same reasons as {@link #isRegisterBankAccessible(TargetRegisterBank)} + */ + @Deprecated boolean isRegisterBankAccessible(TraceThread thread, int frameLevel); /** diff --git a/Ghidra/Debug/Debugger/src/main/resources/defaultTools/Debugger.tool b/Ghidra/Debug/Debugger/src/main/resources/defaultTools/Debugger.tool index 37a33dd7a4..5b4175d6f5 100644 --- a/Ghidra/Debug/Debugger/src/main/resources/defaultTools/Debugger.tool +++ b/Ghidra/Debug/Debugger/src/main/resources/defaultTools/Debugger.tool @@ -12,6 +12,7 @@ + diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java index 9ea8017be6..b2f636c4fc 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java @@ -25,8 +25,7 @@ import java.io.IOException; import java.math.BigInteger; import java.nio.file.Files; import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -455,7 +454,9 @@ public abstract class AbstractGhidraHeadedDebuggerGUITest protected static void performEnabledAction(ActionContextProvider provider, DockingActionIf action, boolean wait) { ActionContext context = waitForValue(() -> { - ActionContext ctx = provider.getActionContext(null); + ActionContext ctx = provider == null + ? new ActionContext() + : provider.getActionContext(null); if (!action.isEnabledForContext(ctx)) { return null; } @@ -529,8 +530,18 @@ public abstract class AbstractGhidraHeadedDebuggerGUITest if (recorder == null) { return; } - waitOn(recorder.getTarget().getModel().flushEvents()); - waitOn(recorder.flushTransactions()); + try { + waitOn(recorder.getTarget().getModel().flushEvents()); + } + catch (RejectedExecutionException e) { + // Whatever + } + try { + waitOn(recorder.flushTransactions()); + } + catch (RejectedExecutionException e) { + // Whatever + } waitForDomainObject(recorder.getTrace()); } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPluginTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPluginTest.java new file mode 100644 index 0000000000..c2a59d0d9f --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPluginTest.java @@ -0,0 +1,583 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.control; + +import static org.junit.Assert.*; + +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.Range; + +import docking.ActionContext; +import docking.action.DockingAction; +import docking.action.DockingActionIf; +import docking.dnd.GClipboard; +import docking.widgets.OptionDialog; +import generic.Unique; +import ghidra.app.plugin.assembler.*; +import ghidra.app.plugin.core.assembler.AssemblerPlugin; +import ghidra.app.plugin.core.assembler.AssemblerPluginTestHelper; +import ghidra.app.plugin.core.clipboard.ClipboardPlugin; +import ghidra.app.plugin.core.codebrowser.CodeViewerProvider; +import ghidra.app.plugin.core.debug.disassemble.DebuggerDisassemblerPlugin; +import ghidra.app.plugin.core.debug.disassemble.DebuggerDisassemblerPluginTestHelper; +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; +import ghidra.app.plugin.core.debug.mapping.DebuggerTargetTraceMapper; +import ghidra.app.plugin.core.debug.mapping.ObjectBasedDebuggerTargetTraceMapper; +import ghidra.app.plugin.core.debug.service.editing.DebuggerStateEditingServicePlugin; +import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePlugin; +import ghidra.app.services.*; +import ghidra.app.services.DebuggerEmulationService.CachedEmulator; +import ghidra.app.services.DebuggerEmulationService.EmulationResult; +import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; +import ghidra.dbg.model.*; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetSteppable.TargetStepKind; +import ghidra.pcode.exec.SuspendedPcodeExecutionException; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.ShortDataType; +import ghidra.program.model.lang.CompilerSpecID; +import ghidra.program.model.lang.LanguageID; +import ghidra.program.model.listing.Instruction; +import ghidra.program.util.ProgramLocation; +import ghidra.trace.database.DBTraceUtils; +import ghidra.trace.model.memory.TraceMemoryFlag; +import ghidra.trace.model.program.TraceVariableSnapProgramView; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.schedule.Scheduler; +import ghidra.trace.model.time.schedule.TraceSchedule; +import ghidra.util.Swing; +import ghidra.util.database.UndoableTransaction; + +/** + * Tests for target control and state editing + * + *

    + * In these and other machine-state-editing integration tests, we use + * {@link StateEditingMode#WRITE_EMULATOR} as a stand-in for any mode. We also use + * {@link StateEditingMode#READ_ONLY} just to verify the mode is heeded. Other modes may be tested + * if bugs crop up in various combinations. + */ +public class DebuggerControlPluginTest extends AbstractGhidraHeadedDebuggerGUITest { + + DebuggerListingPlugin listingPlugin; + DebuggerStateEditingService editingService; + DebuggerEmulationService emulationService; + DebuggerControlPlugin controlPlugin; + + List commands = Collections.synchronizedList(new ArrayList<>()); + + @Before + public void setUpControlTest() throws Exception { + listingPlugin = addPlugin(tool, DebuggerListingPlugin.class); + editingService = addPlugin(tool, DebuggerStateEditingServicePlugin.class); + emulationService = addPlugin(tool, DebuggerEmulationServicePlugin.class); + controlPlugin = addPlugin(tool, DebuggerControlPlugin.class); + + mb = new TestDebuggerModelBuilder() { + @Override + protected TestDebuggerObjectModel newModel(String typeHint) { + commands.clear(); + return new TestDebuggerObjectModel(typeHint) { + @Override + protected TestTargetThread newTestTargetThread( + TestTargetThreadContainer container, int tid) { + return new TestTargetThread(container, tid) { + { + setState(TargetExecutionState.STOPPED); + } + + @Override + public CompletableFuture resume() { + commands.add("resume"); + setState(TargetExecutionState.RUNNING); + return super.resume(); + } + + @Override + public CompletableFuture interrupt() { + commands.add("interrupt"); + setState(TargetExecutionState.STOPPED); + return super.interrupt(); + } + + @Override + public CompletableFuture kill() { + commands.add("kill"); + setState(TargetExecutionState.TERMINATED); + return super.kill(); + } + + @Override + public CompletableFuture step(TargetStepKind kind) { + commands.add("step(" + kind + ")"); + setState(TargetExecutionState.RUNNING); + setState(TargetExecutionState.STOPPED); + return super.step(kind); + } + }; + } + + @Override + public CompletableFuture close() { + commands.add("close"); + return super.close(); + } + }; + } + }; + } + + @Override + protected DebuggerTargetTraceMapper createTargetTraceMapper(TargetObject target) + throws Exception { + return new ObjectBasedDebuggerTargetTraceMapper(target, + new LanguageID("DATA:BE:64:default"), new CompilerSpecID("pointer64"), Set.of()); + } + + @Override + protected TraceRecorder recordAndWaitSync() throws Throwable { + TraceRecorder recorder = super.recordAndWaitSync(); + useTrace(recorder.getTrace()); + return recorder; + } + + @Override + protected TargetObject chooseTarget() { + return mb.testModel.session; + } + + @Test + public void testTargetResumeAction() throws Throwable { + createTestModel(); + TraceRecorder recorder = recordAndWaitSync(); + traceManager.openTrace(tb.trace); + traceManager.activateTrace(tb.trace); + waitForSwing(); + waitOn(recorder.requestFocus(mb.testThread1)); + waitRecorder(recorder); + waitForSwing(); + + performEnabledAction(null, controlPlugin.actionTargetResume, true); + waitRecorder(recorder); + assertEquals(List.of("resume"), commands); + waitForSwing(); + assertFalse(controlPlugin.actionTargetResume.isEnabled()); + } + + @Test + public void testTargetInterruptAction() throws Throwable { + createTestModel(); + TraceRecorder recorder = recordAndWaitSync(); + traceManager.openTrace(tb.trace); + traceManager.activateTrace(tb.trace); + waitForSwing(); + waitOn(recorder.requestFocus(mb.testThread1)); + waitRecorder(recorder); + waitForSwing(); + + assertFalse(controlPlugin.actionTargetInterrupt.isEnabled()); + waitOn(mb.testThread1.resume()); + waitRecorder(recorder); + commands.clear(); + waitForSwing(); + + performEnabledAction(null, controlPlugin.actionTargetInterrupt, true); + waitRecorder(recorder); + assertEquals(List.of("interrupt"), commands); + waitForSwing(); + assertFalse(controlPlugin.actionTargetInterrupt.isEnabled()); + } + + @Test + public void testTargetKillAction() throws Throwable { + createTestModel(); + TraceRecorder recorder = recordAndWaitSync(); + traceManager.openTrace(tb.trace); + traceManager.activateTrace(tb.trace); + waitForSwing(); + waitOn(recorder.requestFocus(mb.testThread1)); + waitRecorder(recorder); + waitForSwing(); + + performEnabledAction(null, controlPlugin.actionTargetKill, true); + waitRecorder(recorder); + assertEquals(List.of("kill"), commands); + waitForSwing(); + assertFalse(controlPlugin.actionTargetKill.isEnabled()); + } + + @Test + public void testTargetDisconnectAction() throws Throwable { + createTestModel(); + TraceRecorder recorder = recordAndWaitSync(); + traceManager.openTrace(tb.trace); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + performEnabledAction(null, controlPlugin.actionTargetDisconnect, true); + waitRecorder(recorder); + assertEquals(List.of("close"), commands); + waitForSwing(); + waitForPass(() -> assertFalse(controlPlugin.actionTargetDisconnect.isEnabled())); + } + + protected void runTestTargetStepAction(DockingAction action, TargetStepKind expected) + throws Throwable { + createTestModel(); + TraceRecorder recorder = recordAndWaitSync(); + traceManager.openTrace(tb.trace); + traceManager.activateTrace(tb.trace); + waitForSwing(); + waitOn(recorder.requestFocus(mb.testThread1)); + waitRecorder(recorder); + waitForSwing(); + + performEnabledAction(null, action, true); + waitRecorder(recorder); + assertEquals(List.of("step(" + expected + ")"), commands); + waitForSwing(); + assertTrue(action.isEnabled()); + } + + @Test + public void testTargetStepIntoAction() throws Throwable { + runTestTargetStepAction(controlPlugin.actionTargetStepInto, TargetStepKind.INTO); + } + + @Test + public void testTargetStepOverAction() throws Throwable { + runTestTargetStepAction(controlPlugin.actionTargetStepOver, TargetStepKind.OVER); + } + + @Test + public void testTargetStepFinishAction() throws Throwable { + runTestTargetStepAction(controlPlugin.actionTargetStepFinish, TargetStepKind.FINISH); + } + + TraceThread createToyLoopTrace() throws Throwable { + createAndOpenTrace(); + + Address start = tb.addr(0x00400000); + TraceThread thread; + try (UndoableTransaction tid = tb.startTransaction()) { + Assembler asm = Assemblers.getAssembler(tb.language); + AssemblyBuffer buf = new AssemblyBuffer(asm, start); + buf.assemble("br 0x" + start); + + thread = tb.getOrAddThread("Threads[0]", 0); + tb.exec(0, thread, 0, "pc = 0x" + start + ";"); + tb.trace.getMemoryManager().putBytes(0, start, ByteBuffer.wrap(buf.getBytes())); + } + return thread; + } + + @Test + public void testEmulateResumeAction() throws Throwable { + TraceThread thread = createToyLoopTrace(); + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + + traceManager.activateThread(thread); + waitForSwing(); + + performEnabledAction(null, controlPlugin.actionEmulateResume, true); + waitForPass(() -> assertFalse(controlPlugin.actionEmulateResume.isEnabled())); + + CachedEmulator ce = Unique.assertOne(emulationService.getBusyEmulators()); + ce.emulator().setSuspended(true); + waitForTasks(); + assertTrue(controlPlugin.actionEmulateResume.isEnabled()); + } + + @Test + public void testEmulateInterruptAction() throws Throwable { + TraceThread thread = createToyLoopTrace(); + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + + traceManager.activateThread(thread); + waitForSwing(); + + assertFalse(controlPlugin.actionEmulateInterrupt.isEnabled()); + + CompletableFuture future = emulationService.backgroundRun(tb.host, + TraceSchedule.snap(0), Scheduler.oneThread(thread)); + waitForPass(() -> assertTrue(controlPlugin.actionEmulateInterrupt.isEnabled())); + + performEnabledAction(null, controlPlugin.actionEmulateInterrupt, true); + EmulationResult result = waitOn(future); + assertTrue(result.error() instanceof SuspendedPcodeExecutionException); + waitForTasks(); + + assertFalse(controlPlugin.actionEmulateInterrupt.isEnabled()); + } + + @Test + public void testEmulateStepBackAction() throws Throwable { + TraceThread thread = createToyLoopTrace(); + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + + traceManager.activateThread(thread); + waitForSwing(); + + assertFalse(controlPlugin.actionEmulateStepBack.isEnabled()); + + traceManager.activateTime(TraceSchedule.parse("0:t0-1")); + waitForSwing(); + + performEnabledAction(null, controlPlugin.actionEmulateStepBack, true); + + assertEquals(TraceSchedule.snap(0), traceManager.getCurrent().getTime()); + assertFalse(controlPlugin.actionEmulateStepBack.isEnabled()); + } + + @Test + public void testEmulateStepIntoAction() throws Throwable { + TraceThread thread = createToyLoopTrace(); + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + + traceManager.activateThread(thread); + waitForSwing(); + + performEnabledAction(null, controlPlugin.actionEmulateStepInto, true); + + assertEquals(TraceSchedule.parse("0:t0-1"), traceManager.getCurrent().getTime()); + } + + @Test + public void testEmulateSkipOverAction() throws Throwable { + TraceThread thread = createToyLoopTrace(); + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR); + + traceManager.activateThread(thread); + waitForSwing(); + + performEnabledAction(null, controlPlugin.actionEmulateSkipOver, true); + + assertEquals(TraceSchedule.parse("0:t0-s1"), traceManager.getCurrent().getTime()); + } + + protected void create2SnapTrace() throws Throwable { + createAndOpenTrace(); + + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getTimeManager().getSnapshot(1, true); + } + } + + @Test + public void testTraceSnapBackwardAction() throws Throwable { + create2SnapTrace(); + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertFalse(controlPlugin.actionTraceSnapBackward.isEnabled()); + + traceManager.activateTime(TraceSchedule.snap(1)); + performEnabledAction(null, controlPlugin.actionTraceSnapBackward, true); + + assertEquals(TraceSchedule.snap(0), traceManager.getCurrent().getTime()); + assertFalse(controlPlugin.actionTraceSnapBackward.isEnabled()); + } + + @Test + public void testTraceSnapForwardAction() throws Throwable { + create2SnapTrace(); + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + + performEnabledAction(null, controlPlugin.actionTraceSnapForward, true); + + assertEquals(TraceSchedule.snap(1), traceManager.getCurrent().getTime()); + assertFalse(controlPlugin.actionTraceSnapForward.isEnabled()); + } + + @Test + public void testPatchInstructionActionInDynamicListingEmu() throws Throwable { + DebuggerDisassemblerPlugin disassemblerPlugin = + addPlugin(tool, DebuggerDisassemblerPlugin.class); + + assertFalse(controlPlugin.actionEditMode.isEnabled()); + + createAndOpenTrace(); + TraceVariableSnapProgramView view = tb.trace.getProgramView(); + try (UndoableTransaction tid = tb.startTransaction()) { + tb.getOrAddThread("Threads[0]", 0); + tb.trace.getMemoryManager() + .createRegion("Memory[bin:.text]", 0, tb.range(0x00400000, 0x00401000), + Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); + // Dynamic Patch Instruction requires existing code unit for context + tb.addInstruction(0, tb.addr(0x00400123), tb.host); + } + + CodeViewerProvider listingProvider = listingPlugin.getProvider(); + DebuggerDisassemblerPluginTestHelper helper = + new DebuggerDisassemblerPluginTestHelper(disassemblerPlugin, listingProvider, view); + + traceManager.activateTrace(tb.trace); + Swing.runNow( + () -> listingProvider.goTo(view, new ProgramLocation(view, tb.addr(0x00400123)))); + waitForSwing(); + + assertTrue(controlPlugin.actionEditMode.isEnabled()); + + runSwing(() -> controlPlugin.actionEditMode + .setCurrentActionStateByUserData(StateEditingMode.READ_ONLY)); + assertEquals(StateEditingMode.READ_ONLY, editingService.getCurrentMode(tb.trace)); + assertFalse( + helper.patchInstructionAction.isAddToPopup(listingProvider.getActionContext(null))); + + runSwing(() -> controlPlugin.actionEditMode + .setCurrentActionStateByUserData(StateEditingMode.WRITE_EMULATOR)); + assertEquals(StateEditingMode.WRITE_EMULATOR, editingService.getCurrentMode(tb.trace)); + + assertTrue( + helper.patchInstructionAction.isAddToPopup(listingProvider.getActionContext(null))); + Instruction ins = + helper.patchInstructionAt(tb.addr(0x00400123), "imm r0,#0x0", "imm r0,#0x3d2"); + assertEquals(2, ins.getLength()); + + long snap = traceManager.getCurrent().getViewSnap(); + assertTrue(DBTraceUtils.isScratch(snap)); + byte[] bytes = new byte[2]; + view.getMemory().getBytes(tb.addr(0x00400123), bytes); + assertArrayEquals(tb.arr(0x30, 0xd2), bytes); + } + + @Test + public void testPatchDataActionInDynamicListingEmu() throws Throwable { + AssemblerPlugin assemblerPlugin = addPlugin(tool, AssemblerPlugin.class); + + assertFalse(controlPlugin.actionEditMode.isEnabled()); + + createAndOpenTrace(); + TraceVariableSnapProgramView view = tb.trace.getProgramView(); + try (UndoableTransaction tid = tb.startTransaction()) { + tb.getOrAddThread("Threads[0]", 0); + tb.trace.getMemoryManager() + .createRegion("Memory[bin:.text]", 0, tb.range(0x00400000, 0x00401000), + Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); + tb.trace.getCodeManager() + .definedData() + .create(Range.atLeast(0L), tb.addr(0x00400123), ShortDataType.dataType); + } + + CodeViewerProvider listingProvider = listingPlugin.getProvider(); + AssemblerPluginTestHelper helper = + new AssemblerPluginTestHelper(assemblerPlugin, listingProvider, view); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertTrue(controlPlugin.actionEditMode.isEnabled()); + + runSwing(() -> controlPlugin.actionEditMode + .setCurrentActionStateByUserData(StateEditingMode.READ_ONLY)); + assertEquals(StateEditingMode.READ_ONLY, editingService.getCurrentMode(tb.trace)); + assertFalse(helper.patchDataAction.isAddToPopup(listingProvider.getActionContext(null))); + + runSwing(() -> controlPlugin.actionEditMode + .setCurrentActionStateByUserData(StateEditingMode.WRITE_EMULATOR)); + assertEquals(StateEditingMode.WRITE_EMULATOR, editingService.getCurrentMode(tb.trace)); + + goTo(listingProvider.getListingPanel(), new ProgramLocation(view, tb.addr(0x00400123))); + assertTrue(helper.patchDataAction.isAddToPopup(listingProvider.getActionContext(null))); + + /** + * TODO: There's a bug in the trace forking: Data units are not replaced when bytes changed. + * Thus, we'll make no assertions about the data unit. + */ + /*Data data =*/ helper.patchDataAt(tb.addr(0x00400123), "0h", "5h"); + // assertEquals(2, data.getLength()); + + long snap = traceManager.getCurrent().getViewSnap(); + assertTrue(DBTraceUtils.isScratch(snap)); + byte[] bytes = new byte[2]; + view.getMemory().getBytes(tb.addr(0x00400123), bytes); + assertArrayEquals(tb.arr(0, 5), bytes); + } + + @Test + public void testPasteActionInDynamicListingEmu() throws Throwable { + addPlugin(tool, ClipboardPlugin.class); + + CodeViewerProvider listingProvider = listingPlugin.getProvider(); + DockingActionIf pasteAction = getLocalAction(listingProvider, "Paste"); + + assertFalse(controlPlugin.actionEditMode.isEnabled()); + + createAndOpenTrace(); + TraceVariableSnapProgramView view = tb.trace.getProgramView(); + try (UndoableTransaction tid = tb.startTransaction()) { + tb.getOrAddThread("Threads[0]", 0); + tb.trace.getMemoryManager() + .createRegion("Memory[bin:.text]", 0, tb.range(0x00400000, 0x00401000), + Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); + } + + traceManager.activateTrace(tb.trace); + waitForSwing(); + + ActionContext ctx; + + assertTrue(controlPlugin.actionEditMode.isEnabled()); + + runSwing(() -> controlPlugin.actionEditMode + .setCurrentActionStateByUserData(StateEditingMode.READ_ONLY)); + assertEquals(StateEditingMode.READ_ONLY, editingService.getCurrentMode(tb.trace)); + ctx = listingProvider.getActionContext(null); + assertTrue(pasteAction.isAddToPopup(ctx)); + assertFalse(pasteAction.isEnabledForContext(ctx)); + + runSwing(() -> controlPlugin.actionEditMode + .setCurrentActionStateByUserData(StateEditingMode.WRITE_EMULATOR)); + assertEquals(StateEditingMode.WRITE_EMULATOR, editingService.getCurrentMode(tb.trace)); + + goTo(listingPlugin.getListingPanel(), new ProgramLocation(view, tb.addr(0x00400123))); + ctx = listingProvider.getActionContext(null); + assertTrue(pasteAction.isAddToPopup(ctx)); + assertFalse(pasteAction.isEnabledForContext(ctx)); + + Clipboard clipboard = GClipboard.getSystemClipboard(); + clipboard.setContents(new StringSelection("12 34 56 78"), null); + ctx = listingProvider.getActionContext(null); + assertTrue(pasteAction.isAddToPopup(ctx)); + assertTrue(pasteAction.isEnabledForContext(ctx)); + + performAction(pasteAction, listingProvider, false); + OptionDialog confirm = waitForDialogComponent(OptionDialog.class); + pressButtonByText(confirm, "Yes"); + + byte[] bytes = new byte[4]; + waitForPass(noExc(() -> { + long snap = traceManager.getCurrent().getViewSnap(); + assertTrue(DBTraceUtils.isScratch(snap)); + view.getMemory().getBytes(tb.addr(0x00400123), bytes); + assertArrayEquals(tb.arr(0x12, 0x34, 0x56, 0x78), bytes); + })); + } +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/editing/DebuggerStateEditingPluginIntegrationTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/editing/DebuggerStateEditingPluginIntegrationTest.java deleted file mode 100644 index a2051b465e..0000000000 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/editing/DebuggerStateEditingPluginIntegrationTest.java +++ /dev/null @@ -1,240 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.plugin.core.debug.gui.editing; - -import static org.junit.Assert.*; - -import java.awt.datatransfer.Clipboard; -import java.awt.datatransfer.StringSelection; -import java.util.Set; - -import org.junit.Test; - -import com.google.common.collect.Range; - -import docking.ActionContext; -import docking.action.DockingActionIf; -import docking.dnd.GClipboard; -import docking.widgets.OptionDialog; -import ghidra.app.plugin.core.assembler.AssemblerPlugin; -import ghidra.app.plugin.core.assembler.AssemblerPluginTestHelper; -import ghidra.app.plugin.core.clipboard.ClipboardPlugin; -import ghidra.app.plugin.core.codebrowser.CodeViewerProvider; -import ghidra.app.plugin.core.debug.disassemble.DebuggerDisassemblerPlugin; -import ghidra.app.plugin.core.debug.disassemble.DebuggerDisassemblerPluginTestHelper; -import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; -import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; -import ghidra.app.services.DebuggerStateEditingService; -import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; -import ghidra.program.model.data.ShortDataType; -import ghidra.program.model.listing.Instruction; -import ghidra.program.util.ProgramLocation; -import ghidra.trace.database.DBTraceUtils; -import ghidra.trace.model.memory.TraceMemoryFlag; -import ghidra.trace.model.program.TraceVariableSnapProgramView; -import ghidra.util.Swing; -import ghidra.util.database.UndoableTransaction; - -/** - * Tests for editing machine state that don't naturally fit elsewhere. - * - *

    - * In these and other machine-state-editing integration tests, we use - * {@link StateEditingMode#WRITE_EMULATOR} as a stand-in for any mode. We also use - * {@link StateEditingMode#READ_ONLY} just to verify the mode is heeded. Other modes may be tested - * if bugs crop up in various combinations. - */ -public class DebuggerStateEditingPluginIntegrationTest extends AbstractGhidraHeadedDebuggerGUITest { - @Test - public void testPatchInstructionActionInDynamicListingEmu() throws Throwable { - DebuggerListingPlugin listingPlugin = addPlugin(tool, DebuggerListingPlugin.class); - DebuggerDisassemblerPlugin disassemblerPlugin = - addPlugin(tool, DebuggerDisassemblerPlugin.class); - DebuggerStateEditingPlugin editingPlugin = - addPlugin(tool, DebuggerStateEditingPlugin.class); - DebuggerStateEditingService editingService = - tool.getService(DebuggerStateEditingService.class); - - assertFalse(editingPlugin.actionEditMode.isEnabled()); - - createAndOpenTrace(); - TraceVariableSnapProgramView view = tb.trace.getProgramView(); - try (UndoableTransaction tid = tb.startTransaction()) { - tb.getOrAddThread("Threads[0]", 0); - tb.trace.getMemoryManager() - .createRegion("Memory[bin:.text]", 0, tb.range(0x00400000, 0x00401000), - Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); - // Dynamic Patch Instruction requires existing code unit for context - tb.addInstruction(0, tb.addr(0x00400123), tb.host); - } - - CodeViewerProvider listingProvider = listingPlugin.getProvider(); - DebuggerDisassemblerPluginTestHelper helper = - new DebuggerDisassemblerPluginTestHelper(disassemblerPlugin, listingProvider, view); - - traceManager.activateTrace(tb.trace); - Swing.runNow( - () -> listingProvider.goTo(view, new ProgramLocation(view, tb.addr(0x00400123)))); - waitForSwing(); - - assertTrue(editingPlugin.actionEditMode.isEnabled()); - - runSwing(() -> editingPlugin.actionEditMode - .setCurrentActionStateByUserData(StateEditingMode.READ_ONLY)); - assertEquals(StateEditingMode.READ_ONLY, editingService.getCurrentMode(tb.trace)); - assertFalse( - helper.patchInstructionAction.isAddToPopup(listingProvider.getActionContext(null))); - - runSwing(() -> editingPlugin.actionEditMode - .setCurrentActionStateByUserData(StateEditingMode.WRITE_EMULATOR)); - assertEquals(StateEditingMode.WRITE_EMULATOR, editingService.getCurrentMode(tb.trace)); - - assertTrue( - helper.patchInstructionAction.isAddToPopup(listingProvider.getActionContext(null))); - Instruction ins = - helper.patchInstructionAt(tb.addr(0x00400123), "imm r0,#0x0", "imm r0,#0x3d2"); - assertEquals(2, ins.getLength()); - - long snap = traceManager.getCurrent().getViewSnap(); - assertTrue(DBTraceUtils.isScratch(snap)); - byte[] bytes = new byte[2]; - view.getMemory().getBytes(tb.addr(0x00400123), bytes); - assertArrayEquals(tb.arr(0x30, 0xd2), bytes); - } - - @Test - public void testPatchDataActionInDynamicListingEmu() throws Throwable { - DebuggerListingPlugin listingPlugin = addPlugin(tool, DebuggerListingPlugin.class); - AssemblerPlugin assemblerPlugin = addPlugin(tool, AssemblerPlugin.class); - DebuggerStateEditingPlugin editingPlugin = - addPlugin(tool, DebuggerStateEditingPlugin.class); - DebuggerStateEditingService editingService = - tool.getService(DebuggerStateEditingService.class); - - assertFalse(editingPlugin.actionEditMode.isEnabled()); - - createAndOpenTrace(); - TraceVariableSnapProgramView view = tb.trace.getProgramView(); - try (UndoableTransaction tid = tb.startTransaction()) { - tb.getOrAddThread("Threads[0]", 0); - tb.trace.getMemoryManager() - .createRegion("Memory[bin:.text]", 0, tb.range(0x00400000, 0x00401000), - Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); - tb.trace.getCodeManager() - .definedData() - .create(Range.atLeast(0L), tb.addr(0x00400123), ShortDataType.dataType); - } - - CodeViewerProvider listingProvider = listingPlugin.getProvider(); - AssemblerPluginTestHelper helper = - new AssemblerPluginTestHelper(assemblerPlugin, listingProvider, view); - - traceManager.activateTrace(tb.trace); - waitForSwing(); - - assertTrue(editingPlugin.actionEditMode.isEnabled()); - - runSwing(() -> editingPlugin.actionEditMode - .setCurrentActionStateByUserData(StateEditingMode.READ_ONLY)); - assertEquals(StateEditingMode.READ_ONLY, editingService.getCurrentMode(tb.trace)); - assertFalse(helper.patchDataAction.isAddToPopup(listingProvider.getActionContext(null))); - - runSwing(() -> editingPlugin.actionEditMode - .setCurrentActionStateByUserData(StateEditingMode.WRITE_EMULATOR)); - assertEquals(StateEditingMode.WRITE_EMULATOR, editingService.getCurrentMode(tb.trace)); - - goTo(listingProvider.getListingPanel(), new ProgramLocation(view, tb.addr(0x00400123))); - assertTrue(helper.patchDataAction.isAddToPopup(listingProvider.getActionContext(null))); - - /** - * TODO: There's a bug in the trace forking: Data units are not replaced when bytes changed. - * Thus, we'll make no assertions about the data unit. - */ - /*Data data =*/ helper.patchDataAt(tb.addr(0x00400123), "0h", "5h"); - // assertEquals(2, data.getLength()); - - long snap = traceManager.getCurrent().getViewSnap(); - assertTrue(DBTraceUtils.isScratch(snap)); - byte[] bytes = new byte[2]; - view.getMemory().getBytes(tb.addr(0x00400123), bytes); - assertArrayEquals(tb.arr(0, 5), bytes); - } - - @Test - public void testPasteActionInDynamicListingEmu() throws Throwable { - DebuggerListingPlugin listingPlugin = addPlugin(tool, DebuggerListingPlugin.class); - DebuggerStateEditingPlugin editingPlugin = - addPlugin(tool, DebuggerStateEditingPlugin.class); - addPlugin(tool, ClipboardPlugin.class); - DebuggerStateEditingService editingService = - tool.getService(DebuggerStateEditingService.class); - - CodeViewerProvider listingProvider = listingPlugin.getProvider(); - DockingActionIf pasteAction = getLocalAction(listingProvider, "Paste"); - - assertFalse(editingPlugin.actionEditMode.isEnabled()); - - createAndOpenTrace(); - TraceVariableSnapProgramView view = tb.trace.getProgramView(); - try (UndoableTransaction tid = tb.startTransaction()) { - tb.getOrAddThread("Threads[0]", 0); - tb.trace.getMemoryManager() - .createRegion("Memory[bin:.text]", 0, tb.range(0x00400000, 0x00401000), - Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); - } - - traceManager.activateTrace(tb.trace); - waitForSwing(); - - ActionContext ctx; - - assertTrue(editingPlugin.actionEditMode.isEnabled()); - - runSwing(() -> editingPlugin.actionEditMode - .setCurrentActionStateByUserData(StateEditingMode.READ_ONLY)); - assertEquals(StateEditingMode.READ_ONLY, editingService.getCurrentMode(tb.trace)); - ctx = listingProvider.getActionContext(null); - assertTrue(pasteAction.isAddToPopup(ctx)); - assertFalse(pasteAction.isEnabledForContext(ctx)); - - runSwing(() -> editingPlugin.actionEditMode - .setCurrentActionStateByUserData(StateEditingMode.WRITE_EMULATOR)); - assertEquals(StateEditingMode.WRITE_EMULATOR, editingService.getCurrentMode(tb.trace)); - - goTo(listingPlugin.getListingPanel(), new ProgramLocation(view, tb.addr(0x00400123))); - ctx = listingProvider.getActionContext(null); - assertTrue(pasteAction.isAddToPopup(ctx)); - assertFalse(pasteAction.isEnabledForContext(ctx)); - - Clipboard clipboard = GClipboard.getSystemClipboard(); - clipboard.setContents(new StringSelection("12 34 56 78"), null); - ctx = listingProvider.getActionContext(null); - assertTrue(pasteAction.isAddToPopup(ctx)); - assertTrue(pasteAction.isEnabledForContext(ctx)); - - performAction(pasteAction, listingProvider, false); - OptionDialog confirm = waitForDialogComponent(OptionDialog.class); - pressButtonByText(confirm, "Yes"); - - byte[] bytes = new byte[4]; - waitForPass(noExc(() -> { - long snap = traceManager.getCurrent().getViewSnap(); - assertTrue(DBTraceUtils.isScratch(snap)); - view.getMemory().getBytes(tb.addr(0x00400123), bytes); - assertArrayEquals(tb.arr(0x12, 0x34, 0x56, 0x78), bytes); - })); - } -} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServiceTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServiceTest.java index 4b1d3b6ebb..9cbd4d2521 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServiceTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/emulation/DebuggerEmulationServiceTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.*; import java.math.BigInteger; import java.nio.ByteBuffer; +import java.util.Set; import java.util.concurrent.CompletableFuture; import org.junit.Before; @@ -35,20 +36,26 @@ import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; import ghidra.app.plugin.core.debug.mapping.DebuggerPlatformMapper; import ghidra.app.plugin.core.debug.mapping.DebuggerPlatformOpinion; import ghidra.app.plugin.core.debug.service.platform.DebuggerPlatformServicePlugin; +import ghidra.app.services.DebuggerEmulationService.EmulationResult; import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.pcode.exec.InterruptPcodeExecutionException; import ghidra.pcode.utils.Utils; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.*; +import ghidra.program.model.listing.InstructionIterator; +import ghidra.program.model.listing.ProgramContext; import ghidra.program.model.mem.Memory; import ghidra.program.model.mem.MemoryBlock; import ghidra.program.util.ProgramLocation; import ghidra.trace.model.DefaultTraceLocation; import ghidra.trace.model.Trace; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; import ghidra.trace.model.guest.TracePlatform; import ghidra.trace.model.memory.TraceMemoryManager; import ghidra.trace.model.memory.TraceMemorySpace; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.schedule.Scheduler; import ghidra.trace.model.time.schedule.TraceSchedule; import ghidra.util.database.UndoableTransaction; import ghidra.util.task.TaskMonitor; @@ -335,4 +342,130 @@ public class DebuggerEmulationServiceTest extends AbstractGhidraHeadedDebuggerGU .getUnsignedValue() .toString(16)); } + + @Test + public void testExecutionBreakpoint() throws Exception { + createProgram(); + intoProject(program); + Assembler asm = Assemblers.getAssembler(program); + Memory memory = program.getMemory(); + Address addrText = addr(program, 0x000400000); + Register regPC = program.getRegister("pc"); + Register regR0 = program.getRegister("r0"); + Register regR1 = program.getRegister("r1"); + Register regR2 = program.getRegister("r2"); + Address addrI2; + try (UndoableTransaction tid = UndoableTransaction.start(program, "Initialize")) { + MemoryBlock blockText = memory.createInitializedBlock(".text", addrText, 0x1000, + (byte) 0, TaskMonitor.DUMMY, false); + blockText.setExecute(true); + InstructionIterator ii = asm.assemble(addrText, + "mov r0, r1", + "mov r2, r0"); + ii.next(); + addrI2 = ii.next().getMinAddress(); + program.getProgramContext() + .setValue(regR1, addrText, addrText, new BigInteger("1234", 16)); + } + + programManager.openProgram(program); + waitForSwing(); + codeBrowser.goTo(new ProgramLocation(program, addrText)); + waitForSwing(); + + performEnabledAction(codeBrowser.getProvider(), emulationPlugin.actionEmulateProgram, true); + + Trace trace = traceManager.getCurrentTrace(); + assertNotNull(trace); + + TraceThread thread = Unique.assertOne(trace.getThreadManager().getAllThreads()); + TraceMemorySpace regs = trace.getMemoryManager().getMemoryRegisterSpace(thread, false); + + try (UndoableTransaction tid = UndoableTransaction.start(trace, "Add breakpoint")) { + trace.getBreakpointManager() + .addBreakpoint("Breakpoints[0]", Range.atLeast(0L), addrI2, Set.of(thread), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "test"); + } + + EmulationResult result = emulationPlugin.run(trace.getPlatformManager().getHostPlatform(), + TraceSchedule.snap(0), TaskMonitor.DUMMY, Scheduler.oneThread(thread)); + + assertEquals(TraceSchedule.parse("0:t0-1"), result.schedule()); + assertTrue(result.error() instanceof InterruptPcodeExecutionException); + + long scratch = result.snapshot(); + + assertEquals(new BigInteger("00400002", 16), + regs.getViewValue(scratch, regPC).getUnsignedValue()); + assertEquals(new BigInteger("1234", 16), + regs.getViewValue(scratch, regR0).getUnsignedValue()); + assertEquals(new BigInteger("1234", 16), + regs.getViewValue(scratch, regR1).getUnsignedValue()); + assertEquals(new BigInteger("0", 16), + regs.getViewValue(scratch, regR2).getUnsignedValue()); + } + + @Test + public void testAccessBreakpoint() throws Exception { + createProgram(); + intoProject(program); + Assembler asm = Assemblers.getAssembler(program); + Memory memory = program.getMemory(); + Address addrText = addr(program, 0x000400000); + Register regPC = program.getRegister("pc"); + Register regR0 = program.getRegister("r0"); + Register regR1 = program.getRegister("r1"); + Register regR2 = program.getRegister("r2"); + try (UndoableTransaction tid = UndoableTransaction.start(program, "Initialize")) { + MemoryBlock blockText = memory.createInitializedBlock(".text", addrText, 0x1000, + (byte) 0, TaskMonitor.DUMMY, false); + blockText.setExecute(true); + asm.assemble(addrText, + "store [r0], r1", + "load r2, [r0]"); + ProgramContext ctx = program.getProgramContext(); + ctx.setValue(regR0, addrText, addrText, new BigInteger("1234", 16)); + ctx.setValue(regR1, addrText, addrText, new BigInteger("5678", 16)); + } + + programManager.openProgram(program); + waitForSwing(); + codeBrowser.goTo(new ProgramLocation(program, addrText)); + waitForSwing(); + + performEnabledAction(codeBrowser.getProvider(), emulationPlugin.actionEmulateProgram, true); + + Trace trace = traceManager.getCurrentTrace(); + assertNotNull(trace); + + TraceThread thread = Unique.assertOne(trace.getThreadManager().getAllThreads()); + TraceMemoryManager mem = trace.getMemoryManager(); + TraceMemorySpace regs = mem.getMemoryRegisterSpace(thread, false); + + try (UndoableTransaction tid = UndoableTransaction.start(trace, "Add breakpoint")) { + trace.getBreakpointManager() + .addBreakpoint("Breakpoints[0]", Range.atLeast(0L), addr(trace, 0x1234), + Set.of(thread), Set.of(TraceBreakpointKind.READ), true, "test"); + } + + EmulationResult result = emulationPlugin.run(trace.getPlatformManager().getHostPlatform(), + TraceSchedule.snap(0), TaskMonitor.DUMMY, Scheduler.oneThread(thread)); + + assertEquals(TraceSchedule.parse("0:t0-1"), result.schedule()); + assertTrue(result.error() instanceof InterruptPcodeExecutionException); + + long scratch = result.snapshot(); + + assertEquals(new BigInteger("00400002", 16), + regs.getViewValue(scratch, regPC).getUnsignedValue()); + assertEquals(new BigInteger("1234", 16), + regs.getViewValue(scratch, regR0).getUnsignedValue()); + assertEquals(new BigInteger("5678", 16), + regs.getViewValue(scratch, regR1).getUnsignedValue()); + byte[] arr = new byte[8]; + mem.getViewBytes(scratch, addr(trace, 0x1234), ByteBuffer.wrap(arr)); + assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 0, 0x56, 0x78 }, arr); + assertEquals(new BigInteger("0", 16), + regs.getViewValue(scratch, regR2).getUnsignedValue()); + } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetExecutionStateful.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetExecutionStateful.java index 2c225cd8de..b9accd2d84 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetExecutionStateful.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetExecutionStateful.java @@ -37,22 +37,7 @@ public interface TargetExecutionStateful extends TargetObject { * This may apply, e.g., to a GDB "Inferior," which has no yet been used to launch or attach * to a process. */ - INACTIVE { - @Override - public boolean isAlive() { - return false; - } - - @Override - public boolean isRunning() { - return false; - } - - @Override - public boolean isStopped() { - return false; - } - }, + INACTIVE(false, false, false), /** * The object is alive, but its execution state is unspecified @@ -64,42 +49,12 @@ public interface TargetExecutionStateful extends TargetObject { * when all of its threads are stopped. For the clients' sakes, all models should * implement these conventions internally. */ - ALIVE { - @Override - public boolean isAlive() { - return true; - } - - @Override - public boolean isRunning() { - return false; - } - - @Override - public boolean isStopped() { - return false; - } - }, + ALIVE(true, false, false), /** * The object is alive, but not executing */ - STOPPED { - @Override - public boolean isAlive() { - return true; - } - - @Override - public boolean isRunning() { - return false; - } - - @Override - public boolean isStopped() { - return true; - } - }, + STOPPED(true, false, true), /** * The object is alive and executing @@ -109,22 +64,7 @@ public interface TargetExecutionStateful extends TargetObject { * thread is currently executing, waiting on an event, or scheduled for execution. It does * not necessarily mean it is executing on a CPU at this exact moment. */ - RUNNING { - @Override - public boolean isAlive() { - return true; - } - - @Override - public boolean isRunning() { - return true; - } - - @Override - public boolean isStopped() { - return false; - } - }, + RUNNING(true, true, false), /** * The object is no longer alive @@ -134,43 +74,44 @@ public interface TargetExecutionStateful extends TargetObject { * stale handles to objects which may still be queried (e.g., for a process exit code), or * e.g., a GDB "Inferior," which could be re-used to launch or attach to another process. */ - TERMINATED { - @Override - public boolean isAlive() { - return false; - } + TERMINATED(false, false, false); - @Override - public boolean isRunning() { - return false; - } + private final boolean alive; + private final boolean running; + private final boolean stopped; - @Override - public boolean isStopped() { - return false; - } - }; + private TargetExecutionState(boolean alive, boolean running, boolean stopped) { + this.alive = alive; + this.running = running; + this.stopped = stopped; + } /** * Check if this state implies the object is alive * * @return true if alive */ - public abstract boolean isAlive(); + public boolean isAlive() { + return alive; + } /** * Check if this state implies the object is running * * @return true if running */ - public abstract boolean isRunning(); + public boolean isRunning() { + return running; + } /** * Check if this state implies the object is stopped * * @return true if stopped */ - public abstract boolean isStopped(); + public boolean isStopped() { + return stopped; + } } /** diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObject.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObject.java index 4ea91e4ee3..e2e6501f27 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObject.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObject.java @@ -1050,6 +1050,22 @@ public interface TargetObject extends Comparable { return ValueUtils.expectType(obj, cls, this, name, fallback, required); } + /** + * Search the model for a suitable-related object of the given interface + * + * @see TargetObjectSchema#searchForSuitable(Class, List) + * @param the expected type of the interface + * @param cls the class giving the expected type + * @return the found object, or null + */ + public default T getCachedSuitable(Class cls) { + List found = getModel().getRootSchema().searchForSuitable(cls, getPath()); + if (found == null) { + return null; + } + return cls.cast(getModel().getModelValue(found)); + } + /** * Invalidate caches associated with this object, other than those for cached children * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java index d1227d6e9f..8769dc7d0e 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java @@ -22,8 +22,8 @@ import java.util.stream.Stream; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; -import ghidra.dbg.target.TargetMethod; -import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.util.PathPattern; import ghidra.dbg.util.PathPredicates; @@ -570,4 +570,29 @@ public interface TraceObject extends TraceUniqueObject { } return null; } + + /** + * Get the execution state, if applicable, of this object + * + *

    + * This searches for the conventional stateful object defining this object's execution state. If + * such an object does not exist, null is returned. If one does exist, then its execution state + * at the given snap is returned. If that state is null, it is assumed + * {@link TargetExecutionState#INACTIVE}. + * + * @param snap the snap + * @return the state or null + */ + default TargetExecutionState getExecutionState(long snap) { + TraceObject stateful = querySuitableTargetInterface(TargetExecutionStateful.class); + if (stateful == null) { + return null; + } + TraceObjectValue stateVal = + stateful.getAttribute(snap, TargetExecutionStateful.STATE_ATTRIBUTE_NAME); + if (stateVal == null) { + return TargetExecutionState.INACTIVE; + } + return TargetExecutionState.valueOf((String) stateVal.getValue()); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/PatchStep.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/PatchStep.java index 395847e676..597f59fb09 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/PatchStep.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/PatchStep.java @@ -312,7 +312,7 @@ public class PatchStep implements Step { } @Override - public void execute(PcodeThread emuThread, Stepper stepper, TaskMonitor monitor) + public void execute(PcodeThread emuThread, Stepper stepper, TaskMonitor monitor) throws CancelledException { PcodeProgram prog = emuThread.getMachine().compileSleigh("schedule", sleigh + ";"); emuThread.getExecutor().execute(prog, emuThread.getUseropLibrary()); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Scheduler.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Scheduler.java new file mode 100644 index 0000000000..904f1ad2d7 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Scheduler.java @@ -0,0 +1,151 @@ +/* ### + * 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.trace.model.time.schedule; + +import ghidra.pcode.emu.PcodeMachine; +import ghidra.pcode.emu.PcodeThread; +import ghidra.pcode.exec.*; +import ghidra.trace.model.Trace; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.thread.TraceThreadManager; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A generator of an emulator's thread schedule + */ +public interface Scheduler { + + /** + * Create a scheduler that allocates all slices to a single thread + * + * @param thread the thread to schedule + * @return the scheduler + */ + static Scheduler oneThread(TraceThread thread) { + long key = thread == null ? -1 : thread.getKey(); + return new Scheduler() { + @Override + public TickStep nextSlice(Trace trace) { + return new TickStep(key, 1000); + } + }; + } + + interface RunResult { + /** + * Get the actual schedule executed + * + *

    + * It is possible for the machine to be interrupted mid-instruction. If this is the case, + * the trace schedule will indicate the p-code steps taken. + * + * @return the schedule + */ + public TraceSchedule schedule(); + + /** + * Get the error that interrupted execution + * + *

    + * Ideally, this is a {@link InterruptPcodeExecutionException}, indicating a breakpoint + * trapped the emulator, but it could be a number of things: + * + *

      + *
    • An instruction decode error
    • + *
    • An unimplemented instruction
    • + *
    • An unimplemented p-code userop
    • + *
    • An error accessing the machine state
    • + *
    • A runtime error in the implementation of a p-code userop
    • + *
    • A runtime error in the implementation of the emulator, in which case, a bug should be + * filed
    • + *
    + * + * @return the error + */ + public Throwable error(); + } + + /** + * The result of running a machine + */ + record RecordRunResult(TraceSchedule schedule, Throwable error) implements RunResult { + } + + /** + * Get the next step to schedule + * + * @return the (instruction-level) thread and tick count + */ + TickStep nextSlice(Trace trace); + + /** + * Run a machine according to the given schedule until it is interrupted + * + *

    + * This method will drop p-code steps from injections, including those from execution + * breakpoints. The goal is to ensure that the returned schedule can be used to recover the same + * state on a machine without injections. Unfortunately, injections which modify the machine + * state, other than unique variables, will defeat that goal. + * + * @param trace the trace whose threads to schedule + * @param eventThread the first thread to schedule if the scheduler doesn't specify + * @param machine the machine to run + * @param monitor a monitor for cancellation + * @return the result of execution + */ + default RunResult run(Trace trace, TraceThread eventThread, PcodeMachine machine, + TaskMonitor monitor) { + TraceThreadManager tm = trace.getThreadManager(); + TraceSchedule completedSteps = TraceSchedule.snap(0); + PcodeThread emuThread = null; + int completedTicks = 0; + try { + while (true) { + TickStep slice = nextSlice(trace); + eventThread = slice.getThread(tm, eventThread); + emuThread = machine.getThread(eventThread.getPath(), true); + for (int i = 0; i < slice.tickCount; i++) { + monitor.checkCanceled(); + emuThread.stepInstruction(); + completedTicks++; + } + completedSteps = completedSteps.steppedForward(eventThread, completedTicks); + completedTicks = 0; + } + } + catch (PcodeExecutionException e) { + completedSteps = completedSteps.steppedForward(eventThread, completedTicks); + if (emuThread.getInstruction() == null) { + return new RecordRunResult(completedSteps, e); + } + PcodeFrame frame = e.getFrame(); + // Rewind one so stepping retries the op causing the error + int count = frame.count() - 1; + if (frame == null || count == 0) { + // If we've decoded, but could execute the first op, just drop the p-code steps + return new RecordRunResult(completedSteps, e); + } + // The +1 accounts for the decode step + return new RecordRunResult( + completedSteps.steppedPcodeForward(eventThread, count + 1), e); + } + catch (CancelledException e) { + return new RecordRunResult( + completedSteps.steppedForward(eventThread, completedTicks), e); + } + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Sequence.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Sequence.java index 5d37590a55..d35b7ad5c0 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Sequence.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Sequence.java @@ -384,8 +384,8 @@ public class Sequence implements Comparable { * @return the last trace thread stepped during execution * @throws CancelledException if execution is cancelled */ - public TraceThread execute(Trace trace, TraceThread eventThread, PcodeMachine machine, - Stepper stepper, TaskMonitor monitor) throws CancelledException { + public TraceThread execute(Trace trace, TraceThread eventThread, PcodeMachine machine, + Stepper stepper, TaskMonitor monitor) throws CancelledException { TraceThreadManager tm = trace.getThreadManager(); TraceThread thread = eventThread; for (Step step : steps) { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/SkipStep.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/SkipStep.java index f2e2797f21..e877115c3d 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/SkipStep.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/SkipStep.java @@ -66,7 +66,7 @@ public class SkipStep extends AbstractStep { } @Override - public void execute(PcodeThread emuThread, Stepper stepper, TaskMonitor monitor) + public void execute(PcodeThread emuThread, Stepper stepper, TaskMonitor monitor) throws CancelledException { for (int i = 0; i < tickCount; i++) { monitor.incrementProgress(1); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Step.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Step.java index 56dabec3b3..5d4c24fc21 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Step.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Step.java @@ -170,20 +170,20 @@ public interface Step extends Comparable { return compareStep(that).compareTo; } - default TraceThread execute(TraceThreadManager tm, TraceThread eventThread, - PcodeMachine machine, Stepper stepper, TaskMonitor monitor) + default TraceThread execute(TraceThreadManager tm, TraceThread eventThread, + PcodeMachine machine, Stepper stepper, TaskMonitor monitor) throws CancelledException { TraceThread thread = getThread(tm, eventThread); if (machine == null) { // Just performing validation (specifically thread parts) return thread; } - PcodeThread emuThread = machine.getThread(thread.getPath(), true); + PcodeThread emuThread = machine.getThread(thread.getPath(), true); execute(emuThread, stepper, monitor); return thread; } - void execute(PcodeThread emuThread, Stepper stepper, TaskMonitor monitor) + void execute(PcodeThread emuThread, Stepper stepper, TaskMonitor monitor) throws CancelledException; long coalescePatches(Language language, List steps); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Stepper.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Stepper.java index fc21b6865b..9445b40c0c 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Stepper.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Stepper.java @@ -17,44 +17,41 @@ package ghidra.trace.model.time.schedule; import ghidra.pcode.emu.PcodeThread; -public interface Stepper { - @SuppressWarnings("rawtypes") +public interface Stepper { enum Enum implements Stepper { INSTRUCTION { @Override - public void tick(PcodeThread thread) { + public void tick(PcodeThread thread) { thread.stepInstruction(); } @Override - public void skip(PcodeThread thread) { + public void skip(PcodeThread thread) { thread.skipInstruction(); } }, PCODE { @Override - public void tick(PcodeThread thread) { + public void tick(PcodeThread thread) { thread.stepPcodeOp(); } @Override - public void skip(PcodeThread thread) { + public void skip(PcodeThread thread) { thread.skipPcodeOp(); } }; } - @SuppressWarnings("unchecked") - static Stepper instruction() { + static Stepper instruction() { return Enum.INSTRUCTION; } - @SuppressWarnings("unchecked") - static Stepper pcode() { + static Stepper pcode() { return Enum.PCODE; } - void tick(PcodeThread thread); + void tick(PcodeThread thread); - void skip(PcodeThread thread); + void skip(PcodeThread thread); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TickStep.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TickStep.java index 27fd9bbe4d..b552a7e7a3 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TickStep.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TickStep.java @@ -66,7 +66,7 @@ public class TickStep extends AbstractStep { } @Override - public void execute(PcodeThread emuThread, Stepper stepper, TaskMonitor monitor) + public void execute(PcodeThread emuThread, Stepper stepper, TaskMonitor monitor) throws CancelledException { for (int i = 0; i < tickCount; i++) { monitor.incrementProgress(1); 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 8fa8b8ff37..a436e195af 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 @@ -561,6 +561,32 @@ public class TraceSchedule implements Comparable { return new TraceSchedule(snap, ticks, new Sequence()); } + /** + * Compute the schedule resulting from this schedule advanced by the given schedule + * + *

    + * This operation cannot be used to append instruction steps after p-code steps. Thus, if this + * schedule contains any p-code steps and {@code} next has instruction steps, an error will be + * + * @param next the schedule to append. Its snap is ignored. + * @return the complete schedule + * @throws IllegalArgumentException if the result would have instruction steps following p-code + * steps + */ + public TraceSchedule advanced(TraceSchedule next) { + if (this.pSteps.isNop()) { + Sequence ticks = this.steps.clone(); + ticks.advance(next.steps); + return new TraceSchedule(this.snap, ticks, next.pSteps.clone()); + } + else if (next.steps.isNop()) { + Sequence pTicks = this.steps.clone(); + pTicks.advance(next.pSteps); + return new TraceSchedule(this.snap, this.steps.clone(), pTicks); + } + throw new IllegalArgumentException("Cannot have instructions steps following p-code steps"); + } + /** * Get the threads involved in the schedule * diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeMachine.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeMachine.java index 24e9a181da..286aaeed24 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeMachine.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/AbstractPcodeMachine.java @@ -20,7 +20,7 @@ import java.util.*; import ghidra.app.plugin.processors.sleigh.SleighLanguage; import ghidra.pcode.exec.*; import ghidra.pcode.exec.PcodeArithmetic.Purpose; -import ghidra.program.model.address.Address; +import ghidra.program.model.address.*; import ghidra.program.model.lang.Language; import ghidra.util.classfinder.ClassSearcher; @@ -70,7 +70,10 @@ public abstract class AbstractPcodeMachine implements PcodeMachine { protected final Collection> threadsView = Collections.unmodifiableCollection(threads.values()); + protected volatile boolean suspended = false; protected final Map injects = new HashMap<>(); + protected final SparseAddressRangeMap accessBreakpoints = + new SparseAddressRangeMap<>(); /** * Construct a p-code machine with the given language and arithmetic @@ -246,6 +249,11 @@ public abstract class AbstractPcodeMachine implements PcodeMachine { return sharedState; } + @Override + public void setSuspended(boolean suspended) { + this.suspended = suspended; + } + /** * Check for a p-code injection (override) at the given address * @@ -300,4 +308,44 @@ public abstract class AbstractPcodeMachine implements PcodeMachine { """, sleighCondition)); injects.put(address, pcode); } + + @Override + public void addAccessBreakpoint(AddressRange range, AccessKind kind) { + accessBreakpoints.put(range, kind); + } + + @Override + public void clearAccessBreakpoints() { + accessBreakpoints.clear(); + } + + protected void checkLoad(AddressSpace space, T offset) { + if (accessBreakpoints.isEmpty()) { + return; + } + try { + long concrete = arithmetic.toLong(offset, Purpose.LOAD); + if (accessBreakpoints.hasEntry(space.getAddress(concrete), AccessKind::trapsRead)) { + throw new InterruptPcodeExecutionException(null, null); + } + } + catch (ConcretionError e) { + // Consider this not hitting any breakpoint + } + } + + protected void checkStore(AddressSpace space, T offset) { + if (accessBreakpoints.isEmpty()) { + return; + } + try { + long concrete = arithmetic.toLong(offset, Purpose.LOAD); + if (accessBreakpoints.hasEntry(space.getAddress(concrete), AccessKind::trapsWrite)) { + throw new InterruptPcodeExecutionException(null, null); + } + } + catch (ConcretionError e) { + // Consider this not hitting any breakpoint + } + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java index d06a060527..2875a407f7 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java @@ -24,6 +24,7 @@ import ghidra.pcode.exec.*; import ghidra.pcode.exec.PcodeArithmetic.Purpose; import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.*; import ghidra.program.model.listing.Instruction; import ghidra.program.model.pcode.PcodeOp; @@ -157,12 +158,22 @@ public class DefaultPcodeThread implements PcodeThread { @Override public void stepOp(PcodeOp op, PcodeFrame frame, PcodeUseropLibrary library) { - if (suspended) { + if (suspended || thread.machine.suspended) { throw new SuspendedPcodeExecutionException(frame, null); } super.stepOp(op, frame, library); } + @Override + protected void checkLoad(AddressSpace space, T offset) { + thread.checkLoad(space, offset); + } + + @Override + protected void checkStore(AddressSpace space, T offset) { + thread.checkStore(space, offset); + } + @Override protected void branchToAddress(Address target) { thread.overrideCounter(target); @@ -606,4 +617,12 @@ public class DefaultPcodeThread implements PcodeThread { public void clearAllInjects() { injects.clear(); } + + protected void checkLoad(AddressSpace space, T offset) { + machine.checkLoad(space, offset); + } + + protected void checkStore(AddressSpace space, T offset) { + machine.checkStore(space, offset); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeMachine.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeMachine.java index d9744b1292..1348ed1b4f 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeMachine.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/PcodeMachine.java @@ -20,7 +20,11 @@ import java.util.Collection; import ghidra.app.plugin.processors.sleigh.SleighLanguage; import ghidra.pcode.emu.DefaultPcodeThread.PcodeEmulationLibrary; import ghidra.pcode.exec.*; +import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.program.model.pcode.Varnode; /** * A machine which execute p-code on state of an abstract type @@ -29,6 +33,45 @@ import ghidra.program.model.address.Address; */ public interface PcodeMachine { + /** + * The kind of access breakpoint + */ + enum AccessKind { + /** A read access breakpoint */ + R(true, false), + /** A write access breakpoint */ + W(false, true), + /** A read/write access breakpoint */ + RW(true, true); + + private final boolean trapsRead; + private final boolean trapsWrite; + + private AccessKind(boolean trapsRead, boolean trapsWrite) { + this.trapsRead = trapsRead; + this.trapsWrite = trapsWrite; + ; + } + + /** + * Check if this kind of breakpoint should trap a read, i.e., {@link PcodeOp#LOAD} + * + * @return true to interrupt + */ + public boolean trapsRead() { + return trapsRead; + } + + /** + * Check if this kind of breakpoint should trap a write, i.e., {@link PcodeOp#STORE} + * + * @return true to interrupt + */ + public boolean trapsWrite() { + return trapsWrite; + } + } + /** * Get the machine's Sleigh language (processor model) * @@ -112,6 +155,13 @@ public interface PcodeMachine { */ PcodeExecutorState getSharedState(); + /** + * Set the suspension state of the machine + * + * @see PcodeThread#setSuspended(boolean) + */ + void setSuspended(boolean suspended); + /** * Compile the given Sleigh code for execution by a thread of this machine * @@ -130,8 +180,8 @@ public interface PcodeMachine { * *

    * This will attempt to compile the given source against this machine's userop library and then - * will inject it at the given address. The resulting p-code replaces that which would - * be executed by decoding the instruction at the given address. The means the machine will not + * inject it at the given address. The resulting p-code replaces that which would be + * executed by decoding the instruction at the given address. The means the machine will not * decode, nor advance its counter, unless the Sleigh causes it. In most cases, the Sleigh will * call {@link PcodeEmulationLibrary#emu_exec_decoded()} to cause the machine to decode and * execute the overridden instruction. @@ -141,6 +191,11 @@ public interface PcodeMachine { * replaced and the old inject completely forgotten. The injector does not support chaining or * double-wrapping, etc. * + *

    + * No synchronization is provided on the internal injection storage. Clients should ensure the + * machine is not executing when injecting p-code. Additionally, the client must ensure only one + * thread is injecting p-code to the machine at a time. + * * @param address the address to inject at * @param source the Sleigh source to compile and inject */ @@ -155,19 +210,65 @@ public interface PcodeMachine { /** * Remove all injects from this machine + * + *

    + * This will clear execution breakpoints, but not access breakpoints. See + * {@link #clearAccessBreakpoints()}. */ void clearAllInjects(); /** - * Add a (conditional) breakpoint at the given address + * Add a conditional execution breakpoint at the given address * *

    * Breakpoints are implemented at the p-code level using an inject, without modification to the * emulated image. As such, it cannot coexist with another inject. A client needing to break * during an inject must use {@link PcodeEmulationLibrary#emu_swi()} in the injected Sleigh. * + *

    + * No synchronization is provided on the internal breakpoint storage. Clients should ensure the + * machine is not executing when adding breakpoints. Additionally, the client must ensure only + * one thread is adding breakpoints to the machine at a time. + * * @param address the address at which to break * @param sleighCondition a Sleigh expression which controls the breakpoint */ void addBreakpoint(Address address, String sleighCondition); + + /** + * Add an access breakpoint over the given range + * + *

    + * Access breakpoints are implemented out of band, without modification to the emulated image. + * The breakpoints are only effective for p-code {@link PcodeOp#LOAD} and {@link PcodeOp#STORE} + * operations with concrete offsets. Thus, an operation that refers directly to a memory + * address, e.g., a memory-mapped register, will not be trapped. Similarly, access breakpoints + * on registers or unique variables will not work. Access to an abstract offset that cannot be + * made concrete, i.e., via {@link PcodeArithmetic#toConcrete(Object, Purpose)} cannot be + * trapped. To interrupt on direct and/or abstract accesses, consider wrapping the relevant + * state and/or overriding {@link PcodeExecutorStatePiece#getVar(Varnode, Reason)} and related. + * For accesses to abstract offsets, consider overriding + * {@link AbstractPcodeMachine#checkLoad(AddressSpace, Object)} and/or + * {@link AbstractPcodeMachine#checkStore(AddressSpace, Object)} instead. + * + *

    + * A breakpoint's range cannot cross more than one page boundary. Pages are 4096 bytes each. + * This allows implementations to optimize checking for breakpoints. If a breakpoint does not + * follow this rule, the behavior is undefined. Breakpoints may overlap, but currently no + * indication is given as to which breakpoint interrupted emulation. + * + *

    + * No synchronization is provided on the internal breakpoint storage. Clients should ensure the + * machine is not executing when adding breakpoints. Additionally, the client must ensure only + * one thread is adding breakpoints to the machine at a time. + * + * @param range the address range to trap + * @param kind the kind of access to trap + */ + void addAccessBreakpoint(AddressRange range, AccessKind kind); + + /** + * Remove all access breakpoints from this machine + */ + void clearAccessBreakpoints(); } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/SparseAddressRangeMap.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/SparseAddressRangeMap.java new file mode 100644 index 0000000000..dc20d6bdd9 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/SparseAddressRangeMap.java @@ -0,0 +1,118 @@ +/* ### + * 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.pcode.emu; + +import java.util.*; +import java.util.Map.Entry; +import java.util.function.Predicate; + +import ghidra.program.model.address.*; + +public class SparseAddressRangeMap { + public static final long PAGE_BITS = 12; + public static final long PAGE_MASK = -1L << PAGE_BITS; + public static final long OFF_MASK = ~PAGE_MASK; + + private static class Space { + private final Map> pages = new HashMap<>(); + + private static long getPageIndex(Address addr) { + return addr.getOffset() >> PAGE_BITS; + } + + Entry put(Entry entry) { + AddressRange range = entry.getKey(); + long indexMin = getPageIndex(range.getMinAddress()); + Page pageMin = pages.computeIfAbsent(indexMin, o -> new Page<>()); + pageMin.put(entry); + long indexMax = getPageIndex(range.getMaxAddress()); + if (indexMax == indexMin) { + return entry; + } + Page pageMax = pages.computeIfAbsent(indexMax, o -> new Page<>()); + return pageMax.put(entry); + } + + boolean hasEntry(Address address, Predicate predicate) { + Page page = pages.get(getPageIndex(address)); + if (page == null) { + return false; + } + return page.hasEntry(address, predicate); + } + } + + private static class Page { + static final Comparator> ENTRY_COMPARATOR = Page::compareEntries; + private final List> entries = new ArrayList<>(); + + private static int compareEntries(Entry e1, Entry e2) { + return e1.getKey().getMinAddress().compareTo(e2.getKey().getMinAddress()); + } + + Entry put(Entry entry) { + int index = Collections.binarySearch(entries, entry, ENTRY_COMPARATOR); + if (index < 0) { + index = -index - 1; + } + entries.add(index, entry); + return entry; + } + + boolean hasEntry(Address address, Predicate predicate) { + for (Entry ent : entries) { + AddressRange range = ent.getKey(); + if (range.contains(address)) { + if (predicate.test(ent.getValue())) { + return true; + } + continue; + } + if (address.compareTo(range.getMinAddress()) < 0) { + return false; + } + } + return false; + } + } + + private final Map> spaces = new HashMap<>(); + private boolean isEmpty = true; + + public Entry put(AddressRange range, V value) { + Space space = spaces.computeIfAbsent(range.getAddressSpace(), s -> new Space<>()); + Entry entry = space.put(Map.entry(range, value)); + isEmpty = false; + return entry; + } + + public boolean hasEntry(Address address, Predicate predicate) { + Space space = spaces.get(address.getAddressSpace()); + if (space == null) { + return false; + } + return space.hasEntry(address, predicate); + } + + public void clear() { + spaces.clear(); + isEmpty = true; + } + + public boolean isEmpty() { + return isEmpty; + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java index 457032631d..cc359cc6ac 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java @@ -329,6 +329,15 @@ public class PcodeExecutor { state.setVar(outVar, out); } + /** + * Extension point: logic preceding a load + * + * @param space the address space to be loaded from + * @param offset the offset about to be loaded from + */ + protected void checkLoad(AddressSpace space, T offset) { + } + /** * Execute a load * @@ -339,6 +348,7 @@ public class PcodeExecutor { AddressSpace space = language.getAddressFactory().getAddressSpace(spaceID); Varnode inOffset = op.getInput(1); T offset = state.getVar(inOffset, reason); + checkLoad(space, offset); Varnode outvar = op.getOutput(); T out = state.getVar(space, offset, outvar.getSize(), true, reason); @@ -347,6 +357,15 @@ public class PcodeExecutor { state.setVar(outvar, mod); } + /** + * Extension point: logic preceding a store + * + * @param space the address space to be stored to + * @param offset the offset about to be stored to + */ + protected void checkStore(AddressSpace space, T offset) { + } + /** * Execute a store * @@ -357,6 +376,7 @@ public class PcodeExecutor { AddressSpace space = language.getAddressFactory().getAddressSpace(spaceID); Varnode inOffset = op.getInput(1); T offset = state.getVar(inOffset, reason); + checkStore(space, offset); Varnode valVar = op.getInput(2); T val = state.getVar(valVar, reason); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeFrame.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeFrame.java index f4afc04832..fb130cd6a9 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeFrame.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeFrame.java @@ -130,6 +130,7 @@ public class PcodeFrame { private final List code; private final Map useropNames; + private int count = 0; private int index = 0; private int branched = -1; @@ -157,6 +158,19 @@ public class PcodeFrame { return new MyFormatter().formatOps(language, code); } + /** + * The number of p-code ops executed + * + *

    + * Contrast this to {@link #index()}, which marks the next op to be executed. This counts the + * number of ops executed, which will differ from index when an internal branch is taken. + * + * @return the count + */ + public int count() { + return count; + } + /** * The index of the next p-code op to be executed * @@ -190,6 +204,7 @@ public class PcodeFrame { * @return the value of the index before it was advanced */ public int advance() { + count++; return index++; } @@ -199,6 +214,7 @@ public class PcodeFrame { * @return the value of the index before it was stepped back */ public int stepBack() { + count--; return index--; } diff --git a/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/pcode/emu/SparseAddressRangeMapTest.java b/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/pcode/emu/SparseAddressRangeMapTest.java new file mode 100644 index 0000000000..906a7e84a2 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/pcode/emu/SparseAddressRangeMapTest.java @@ -0,0 +1,80 @@ +/* ### + * 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.pcode.emu; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import ghidra.program.model.address.*; + +public class SparseAddressRangeMapTest { + AddressSpace space = new GenericAddressSpace("test", 64, AddressSpace.TYPE_RAM, 0); + + Address addr(long off) { + return space.getAddress(off); + } + + AddressRange range(long min, long max) { + return new AddressRangeImpl(addr(min), addr(max)); + } + + @Test + public void testIsEmpty() { + SparseAddressRangeMap map = new SparseAddressRangeMap<>(); + assertTrue(map.isEmpty()); + + map.put(range(0x0, 0xff), "Hello!"); + assertFalse(map.isEmpty()); + + map.clear(); + assertTrue(map.isEmpty()); + } + + @Test + public void testHasEntry() { + SparseAddressRangeMap map = new SparseAddressRangeMap<>(); + assertFalse(map.hasEntry(addr(0x0f), "Hello!"::equals)); + + map.put(range(0x0, 0xff), "Hello!"); + assertTrue(map.hasEntry(addr(0x0f), "Hello!"::equals)); + assertFalse(map.hasEntry(addr(0x100), "Hello!"::equals)); + assertFalse(map.hasEntry(addr(0x0f), "Good bye!"::equals)); + + map.clear(); + assertFalse(map.hasEntry(addr(0x0f), "Hello!"::equals)); + } + + @Test + public void testHasEntrySpansPages() { + SparseAddressRangeMap map = new SparseAddressRangeMap<>(); + map.put(range(0x100, 0x1100), "Hello!"); + assertTrue(map.hasEntry(addr(0x0fff), "Hello!"::equals)); + assertTrue(map.hasEntry(addr(0x1000), "Hello!"::equals)); + } + + @Test + public void testHasEntryOverlapping() { + SparseAddressRangeMap map = new SparseAddressRangeMap<>(); + map.put(range(0x0, 0xff), "Hello!"); + map.put(range(0x10, 0x10f), "Good bye!"); + assertTrue(map.hasEntry(addr(0x0f), "Hello!"::equals)); + assertTrue(map.hasEntry(addr(0x20), "Hello!"::equals)); + assertTrue(map.hasEntry(addr(0x20), "Good bye!"::equals)); + assertTrue(map.hasEntry(addr(0x100), "Good bye!"::equals)); + } +}