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 e2ab6a3cc9..223beae5a3 100644 --- a/Ghidra/Debug/Debugger/certification.manifest +++ b/Ghidra/Debug/Debugger/certification.manifest @@ -42,6 +42,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| @@ -109,11 +126,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 0000000000..1bd991dba7 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/2leftarrow.png differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/2rightarrow.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/2rightarrow.png new file mode 100644 index 0000000000..1942c62a25 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/2rightarrow.png differ 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 0000000000..de33d19e82 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/disconnect.png differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/interrupt.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/interrupt.png new file mode 100644 index 0000000000..f6ae4fc5db Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/interrupt.png differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/kill.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/kill.png new file mode 100644 index 0000000000..4599a64450 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/kill.png differ 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 0000000000..b9f6990bff Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/resume.png differ 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 0000000000..3dd8717a12 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/skipover.png differ 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 0000000000..a1fad09fe5 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepback.png differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepinto.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepinto.png new file mode 100644 index 0000000000..4c92124af4 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepinto.png differ 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 0000000000..0c5c187b87 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/steplast.png differ 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 0000000000..1e8886167a Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepout.png differ 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 0000000000..72d995c318 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/stepover.png differ 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 0000000000..bad4477d96 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-disabled.png differ 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 0000000000..53964c75ed Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-emulator.png differ 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 0000000000..37b05b2d51 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-target.png differ 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 0000000000..f3c62d5a2c Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerControlPlugin/images/write-trace.png differ 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 483d165a27..0000000000 Binary files a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-disabled.png and /dev/null differ 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 72600b472c..0000000000 Binary files a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-emulator.png and /dev/null differ 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 c8f0e047ec..0000000000 Binary files a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-target.png and /dev/null differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-trace.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-trace.png deleted file mode 100644 index c0b120cd82..0000000000 Binary files a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStateEditingPlugin/images/write-trace.png and /dev/null differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html index b35676df36..9f89fd5bcc 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html @@ -74,11 +74,9 @@
  • 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)); + } +}