Merge remote-tracking branch

'origin/GP-1595_Dan_globalControlActions--SQUASHED' (Closes #3742)
This commit is contained in:
Ryan Kurtz 2022-10-19 13:03:25 -04:00
commit c17fd389df
66 changed files with 3335 additions and 763 deletions

View file

@ -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;

View file

@ -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<TargetObject, GdbModelTargetInferiorContainer>
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<Void> 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<Void> interrupt() {
return impl.session.interrupt();
}
@Override
public CompletableFuture<Void> attach(TargetAttachable attachable) {
GdbModelTargetAttachable mine = impl.assertMine(GdbModelTargetAttachable.class, attachable);

View file

@ -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<Void> 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

View file

@ -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|

View file

@ -153,9 +153,9 @@
sortgroup="n"
target="help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html" />
<tocdef id="DebuggerStateEditingPlugin" text="Editing Machine State"
<tocdef id="DebuggerControlPlugin" text="Control and Machine State"
sortgroup="n1"
target="help/topics/DebuggerStateEditingPlugin/DebuggerStateEditingPlugin.html" />
target="help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html" />
<tocdef id="DebuggerMemviewPlugin" text="Memview Plot"
sortgroup="o"

View file

@ -0,0 +1,202 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML>
<HEAD>
<META name="generator" content=
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
<TITLE>Debugger: Editing Machine State</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="../../shared/Frontpage.css">
</HEAD>
<BODY lang="EN-US">
<H1><A name="plugin"></A>Debugger: Control and Machine State</H1>
<P>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.</P>
<H2>Actions</H2>
<P>The plugin provides several actions, but only certain ones are displayed, depending on the
current mode.</P>
<H3><A name="edit_mode"></A>Edit Mode</H3>
<P>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:</P>
<UL>
<LI><IMG alt="" src="images/write-disabled.png"> Control Target w/Edits Disabled - This
presents actions for controlling the live target but rejects all machine-state edits.</LI>
<LI><IMG alt="" src="images/write-target.png"> 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.</LI>
<LI><IMG alt="" src="images/write-trace.png"> 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.</LI>
<LI><IMG alt="" src="images/write-emulator.png"> 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 <A href=
"help/topics/DebuggerThreadsPlugin/DebuggerThreadsPlugin.html#goto_time">Go To Time</A>
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.</LI>
</UL>
<H2>Target Control Actions</H2>
<P>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.</P>
<H3><A name="target_resume"></A><IMG alt="" src="images/resume.png"> Resume</H3>
<P>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.</P>
<H3><A name="target_interrupt"></A><IMG alt="" src="images/interrupt.png"> Interrupt</H3>
<P>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.</P>
<H3><A name="target_kill"></A><IMG alt="" src="images/kill.png"> Kill</H3>
<P>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.</P>
<H3><A name="target_disconnect"></A><IMG alt="" src="images/disconnect.png"> Disconnect</H3>
<P>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.</P>
<H3><A name="target_step_into"></A><IMG alt="" src="images/stepinto.png"> Step Into</H3>
<P>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.</P>
<H3><A name="target_step_over"></A><IMG alt="" src="images/stepover.png"> Step Over</H3>
<P>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.</P>
<H3><A name="target_step_finish"></A><IMG alt="" src="images/stepout.png"> Finish</H3>
<P>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.</P>
<H3><A name="target_step_last"></A><IMG alt="" src="images/steplast.png"> Step Repeat Last /
Extended</H3>
<P>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.</P>
<H2>Trace Navigation Actions</H2>
<P>These actions are visible when the "Control Trace" mode is selected. They are available when
there is an active trace.</P>
<H3><A name="trace_snap_backward"></A><IMG alt="" src="images/2leftarrow.png" width="16">
Snapshot Backward</H3>
<P>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.</P>
<H3><A name="trace_snap_forward"></A><IMG alt="" src="images/2rightarrow.png" width="16">
Snapshot Forward</H3>
<P>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.</P>
<H2>Emulation Actions</H2>
<P>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.</P>
<H3><A name="emu_resume"></A><IMG alt="" src="images/resume.png"> Resume</H3>
<P>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.</P>
<H3><A name="emu_interrupt"></A><IMG alt="" src="images/interrupt.png"> Interrupt</H3>
<P>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.</P>
<H3><A name="emu_step_back"></A><IMG alt="" src="images/stepback.png"> Step Back</H3>
<P>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.</P>
<H3><A name="emu_step_into"></A><IMG alt="" src="images/stepinto.png"> Step Into</H3>
<P>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.</P>
<H3><A name="emu_skip_over"></A><IMG alt="" src="images/skipover.png"> Skip Over</H3>
<P>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 <EM>skips</EM> 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 "<EM>Step</EM> Over," which is not currently
supported by the emulator.</P>
<H2>Recommendations</H2>
<P>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.</P>
<P>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: <A href=
"help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html">Dynamic Listing</A>, <A href=
"help/topics/DebuggerMemoryBytesPlugin/DebuggerMemoryBytesPlugin.html">Memory (Dynamic
Bytes)</A>, <A href=
"help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html">Registers</A>, and <A href=
"help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html">Watches</A>.</P>
</BODY>
</HTML>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

View file

@ -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.</P>
<P>The dynamic listing supports editing memory via the <A href=
"help/topics/DebuggerStateEditingPlugin/DebuggerStateEditingPlugin.html">Machine State Editing
Plugin and Service</A>. Such edits are performed as usual: Via the <A href=
<P>The dynamic listing supports editing memory. See <A href=
"help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html">Control and Machine State</A>.
Such edits are performed as usual: Via the <A href=
"help/topics/AssemblerPlugin/Assembler.htm">Patch</A> actions, or by pasting byte strings.
These edits may be directed toward a live target, the trace, or the emulator.</P>

View file

@ -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.</P>
<P>The dynamic listing supports editing memory via the <A href=
"help/topics/DebuggerStateEditingPlugin/DebuggerStateEditingPlugin.html">Machine State Editing
Plugin and Service</A>. 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. <B>NOTE:</B> 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.</P>
<P>The dynamic listing supports editing memory. See<A href=
"help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html">Control and Machine State</A>.
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.
<B>NOTE:</B> 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.</P>
<H2>Actions</H2>

View file

@ -126,7 +126,7 @@
<P>Interrupt the current target's execution.</P>
<H3><A name="resume"></A><IMG alt="" src="images/continue.png"> Resume (Continue, Go)</H3>
<H3><A name="resume"></A><IMG alt="" src="images/resume.png"> Resume (Continue, Go)</H3>
<P>Allow the current target to resume execution.</P>
@ -142,7 +142,7 @@
<P>Allow the current target to finish the current subroutine, pausing after.</P>
<H3><A name="step_last"></A><IMG alt="" src="images/stepout.png"> Step Last / Extended</H3>
<H3><A name="step_last"></A><IMG alt="" src="images/steplast.png"> Step Last / Extended</H3>
<P>Perform a target-defined step, often the last (possibly custom or extended) step.</P>

View file

@ -46,9 +46,7 @@
<LI>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 <B>Enable Edits</B> toggle is
on, and the <A href=
"help/topics/DebuggerStateEditingPlugin/DebuggerStateEditingPlugin.html">editing service</A>
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 <FONT color=
"red">red</FONT>.</LI>
@ -102,9 +100,9 @@
<P>This toggle is a write protector for machine state. To modify register values, this toggle
must be enabled. Edits are directed according the to <A href=
"help/topics/DebuggerStateEditingPlugin/DebuggerStateEditingPlugin.html">State Editing Plugin
and Service</A>. <B>Note:</B> Only the raw "Value" column can be edited directly. The "Repr"
column cannot be edited, yet.</P>
"help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html">Control and Machine State
Plugin</A>. <B>Note:</B> Only the raw "Value" column can be edited directly. The "Repr" column
cannot be edited, yet.</P>
<H3><A name="clone_window"></A>Clone Window</H3>

View file

@ -1,87 +0,0 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML>
<HEAD>
<META name="generator" content=
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
<TITLE>Debugger: Editing Machine State</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="../../shared/Frontpage.css">
</HEAD>
<BODY lang="EN-US">
<H1><A name="plugin"></A>Debugger: Editing Machine State</H1>
<P>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.</P>
<H2>Actions</H2>
<P>The plugin provides a single action:</P>
<H3><A name="edit_mode"></A>Edit Mode</H3>
<P>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:</P>
<UL>
<LI><IMG alt="" src="images/write-disabled.png"> Read-Only - Rejects all edits.</LI>
<LI><IMG alt="" src="images/write-target.png"> 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.</LI>
<LI><IMG alt="" src="images/write-trace.png"> Write Trace - Directs all edits to the trace.
Edits are generally always accepted, and they are applied directly to the trace.</LI>
<LI><IMG alt="" src="images/write-emulator.png"> 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 <A href=
"help/topics/DebuggerThreadsPlugin/DebuggerThreadsPlugin.html#goto_time">Go To Time</A>
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.</LI>
</UL>
<H2>Recommendations</H2>
<P>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.</P>
<P>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 <A href=
"help/topics/DebuggerTraceViewDiffPlugin/DebuggerTraceViewDiffPlugin.html">compared</A>.</P>
<P>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: <A href=
"help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html">Dynamic Listing</A>, <A href=
"help/topics/DebuggerMemoryBytesPlugin/DebuggerMemoryBytesPlugin.html">Memory (Dynamic
Bytes)</A>, <A href=
"help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html">Registers</A>, and <A href=
"help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html">Watches</A>.</P>
</BODY>
</HTML>

View file

@ -74,11 +74,9 @@
<LI>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 <B>Enable Edits</B> toggle
is on, and the <A href=
"help/topics/DebuggerStateEditingPlugin/DebuggerStateEditingPlugin.html">editing service</A>
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 <FONT color="red">red</FONT>.</LI>
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 <FONT color="red">red</FONT>.</LI>
<LI>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 @@
<P>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 <A href=
"help/topics/DebuggerStateEditingPlugin/DebuggerStateEditingPlugin.html">State Editing Plugin
and Service</A>. <B>Note:</B> Only the raw "Value" column can be edited directly. The "Repr"
column cannot be edited, yet.</P>
"help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html">Control and Machine State
Plugin</A>. <B>Note:</B> Only the raw "Value" column can be edited directly. The "Repr" column
cannot be edited, yet.</P>
<H2><A name="colors"></A>Tool Options: Colors</H2>

View file

@ -709,7 +709,7 @@ public class DebuggerCoordinates {
}
public boolean isAlive() {
return recorder != null;
return recorder != null && recorder.isRecording();
}
public boolean isPresent() {

View file

@ -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<StateEditingMode> builder(Plugin owner) {
String ownerName = owner.getName();
return new MultiStateActionBuilder<StateEditingMode>(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";

View file

@ -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<StateEditingMode> 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<StateEditingMode> 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());
}
}
}

View file

@ -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<T> extends Task {
protected final CompletableFuture<T> 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<Long> 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<Long> {
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<EmulationResult> {
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<CachedEmulator, Integer> busy = new LinkedHashMap<>();
protected final ListenerSet<EmulatorStateListener> 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<Long> 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<EmulationResult> 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<? extends TraceSnapshot> 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<Long> 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<TraceBreakpointKind> 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<CacheKey, CachedEmulator> 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<CachedEmulator> 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

View file

@ -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;

View file

@ -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);

View file

@ -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,

View file

@ -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<Void>
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<TraceRecorder> {
@Override
@ -138,9 +174,25 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
Swing.runLater(() -> updateCurrentRecorder());
}
public CompletableFuture<Void> 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<Void> 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 <T> CompletableFuture<T> tryHarder(Supplier<CompletableFuture<T>> 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<T> 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();

View file

@ -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
*
* <p>
* <b>WARNING:</b> 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.
*
* <p>
* 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
*
* <p>
* 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
*
* <p>
* This is the preferred means of performing emulation. Because the underlying emulator may
* request a <em>blocking</em> read from a target, it is important that
* {@link #emulate(TracePlatform, TraceSchedule, TaskMonitor)} is <em>never</em> called by the
* Swing thread.
* This is the preferred means of performing definite emulation. Because the underlying emulator
* may request a <em>blocking</em> read from a target, it is important that
* {@link #emulate(TracePlatform, TraceSchedule, TaskMonitor) emulate} is <em>never</em> 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<Long> 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
*
* <p>
* 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<EmulationResult> backgroundRun(TracePlatform platform, TraceSchedule from,
Scheduler scheduler);
/**
* Get the cached emulator for the given trace and time
*
* <p>
* 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<CachedEmulator> 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);
}

View file

@ -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:
*
* <ol>
* <li>The particular type may not be supported or of interest to the recorder.</li>
* <li>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.</li>
* <li>The target object may not longer exist for a given trace object.</li>
* </ol>
*
* <p>
* 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
*
* <p>
* 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<TargetThread> 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);
/**

View file

@ -12,6 +12,7 @@
<PACKAGE NAME="Ghidra Core">
<INCLUDE CLASS="ghidra.app.plugin.core.editor.TextEditorManagerPlugin" />
<INCLUDE CLASS="ghidra.app.plugin.core.interpreter.InterpreterPanelPlugin" />
<EXCLUDE CLASS="ghidra.plugins.fsbrowser.FileSystemBrowserPlugin" />
</PACKAGE>
<ROOT_NODE X_POS="671" Y_POS="447" WIDTH="1920" HEIGHT="1017" EX_STATE="0" FOCUSED_OWNER="DebuggerConsolePlugin" FOCUSED_NAME="Debug Console" FOCUSED_TITLE="Debug Console">
<SPLIT_NODE WIDTH="1918" HEIGHT="910" DIVIDER_LOCATION="767" ORIENTATION="VERTICAL">

View file

@ -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());
}

View file

@ -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
*
* <p>
* 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<String> 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<Void> resume() {
commands.add("resume");
setState(TargetExecutionState.RUNNING);
return super.resume();
}
@Override
public CompletableFuture<Void> interrupt() {
commands.add("interrupt");
setState(TargetExecutionState.STOPPED);
return super.interrupt();
}
@Override
public CompletableFuture<Void> kill() {
commands.add("kill");
setState(TargetExecutionState.TERMINATED);
return super.kill();
}
@Override
public CompletableFuture<Void> step(TargetStepKind kind) {
commands.add("step(" + kind + ")");
setState(TargetExecutionState.RUNNING);
setState(TargetExecutionState.STOPPED);
return super.step(kind);
}
};
}
@Override
public CompletableFuture<Void> 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<EmulationResult> 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);
}));
}
}

View file

@ -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.
*
* <p>
* 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);
}));
}
}

View file

@ -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());
}
}

View file

@ -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 <em>all</em> 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;
}
}
/**

View file

@ -1050,6 +1050,22 @@ public interface TargetObject extends Comparable<TargetObject> {
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 <T> the expected type of the interface
* @param cls the class giving the expected type
* @return the found object, or null
*/
public default <T extends TargetObject> T getCachedSuitable(Class<T> cls) {
List<String> 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
*

View file

@ -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
*
* <p>
* 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());
}
}

View file

@ -312,7 +312,7 @@ public class PatchStep implements Step {
}
@Override
public <T> void execute(PcodeThread<T> emuThread, Stepper<T> stepper, TaskMonitor monitor)
public <T> void execute(PcodeThread<T> emuThread, Stepper stepper, TaskMonitor monitor)
throws CancelledException {
PcodeProgram prog = emuThread.getMachine().compileSleigh("schedule", sleigh + ";");
emuThread.getExecutor().execute(prog, emuThread.getUseropLibrary());

View file

@ -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
*
* <p>
* 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
*
* <p>
* Ideally, this is a {@link InterruptPcodeExecutionException}, indicating a breakpoint
* trapped the emulator, but it could be a number of things:
*
* <ul>
* <li>An instruction decode error</li>
* <li>An unimplemented instruction</li>
* <li>An unimplemented p-code userop</li>
* <li>An error accessing the machine state</li>
* <li>A runtime error in the implementation of a p-code userop</li>
* <li>A runtime error in the implementation of the emulator, in which case, a bug should be
* filed</li>
* </ul>
*
* @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
*
* <p>
* 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);
}
}
}

View file

@ -384,8 +384,8 @@ public class Sequence implements Comparable<Sequence> {
* @return the last trace thread stepped during execution
* @throws CancelledException if execution is cancelled
*/
public <T> TraceThread execute(Trace trace, TraceThread eventThread, PcodeMachine<T> machine,
Stepper<T> 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) {

View file

@ -66,7 +66,7 @@ public class SkipStep extends AbstractStep {
}
@Override
public <T> void execute(PcodeThread<T> emuThread, Stepper<T> stepper, TaskMonitor monitor)
public <T> void execute(PcodeThread<T> emuThread, Stepper stepper, TaskMonitor monitor)
throws CancelledException {
for (int i = 0; i < tickCount; i++) {
monitor.incrementProgress(1);

View file

@ -170,20 +170,20 @@ public interface Step extends Comparable<Step> {
return compareStep(that).compareTo;
}
default <T> TraceThread execute(TraceThreadManager tm, TraceThread eventThread,
PcodeMachine<T> machine, Stepper<T> 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<T> emuThread = machine.getThread(thread.getPath(), true);
PcodeThread<?> emuThread = machine.getThread(thread.getPath(), true);
execute(emuThread, stepper, monitor);
return thread;
}
<T> void execute(PcodeThread<T> emuThread, Stepper<T> stepper, TaskMonitor monitor)
<T> void execute(PcodeThread<T> emuThread, Stepper stepper, TaskMonitor monitor)
throws CancelledException;
long coalescePatches(Language language, List<Step> steps);

View file

@ -17,44 +17,41 @@ package ghidra.trace.model.time.schedule;
import ghidra.pcode.emu.PcodeThread;
public interface Stepper<T> {
@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 <T> Stepper<T> instruction() {
static Stepper instruction() {
return Enum.INSTRUCTION;
}
@SuppressWarnings("unchecked")
static <T> Stepper<T> pcode() {
static Stepper pcode() {
return Enum.PCODE;
}
void tick(PcodeThread<T> thread);
void tick(PcodeThread<?> thread);
void skip(PcodeThread<T> thread);
void skip(PcodeThread<?> thread);
}

View file

@ -66,7 +66,7 @@ public class TickStep extends AbstractStep {
}
@Override
public <T> void execute(PcodeThread<T> emuThread, Stepper<T> stepper, TaskMonitor monitor)
public <T> void execute(PcodeThread<T> emuThread, Stepper stepper, TaskMonitor monitor)
throws CancelledException {
for (int i = 0; i < tickCount; i++) {
monitor.incrementProgress(1);

View file

@ -561,6 +561,32 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
return new TraceSchedule(snap, ticks, new Sequence());
}
/**
* Compute the schedule resulting from this schedule advanced by the given schedule
*
* <p>
* 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
*

View file

@ -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<T> implements PcodeMachine<T> {
protected final Collection<PcodeThread<T>> threadsView =
Collections.unmodifiableCollection(threads.values());
protected volatile boolean suspended = false;
protected final Map<Address, PcodeProgram> injects = new HashMap<>();
protected final SparseAddressRangeMap<AccessKind> accessBreakpoints =
new SparseAddressRangeMap<>();
/**
* Construct a p-code machine with the given language and arithmetic
@ -246,6 +249,11 @@ public abstract class AbstractPcodeMachine<T> implements PcodeMachine<T> {
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<T> implements PcodeMachine<T> {
""", 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
}
}
}

View file

@ -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<T> implements PcodeThread<T> {
@Override
public void stepOp(PcodeOp op, PcodeFrame frame, PcodeUseropLibrary<T> 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<T> implements PcodeThread<T> {
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);
}
}

View file

@ -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<T> {
/**
* 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<T> {
*/
PcodeExecutorState<T> 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<T> {
*
* <p>
* 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 <em>replaces</em> 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 <em>replaces</em> 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<T> {
* replaced and the old inject completely forgotten. The injector does not support chaining or
* double-wrapping, etc.
*
* <p>
* 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<T> {
/**
* Remove all injects from this machine
*
* <p>
* 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
*
* <p>
* 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.
*
* <p>
* 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
*
* <p>
* 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.
*
* <p>
* 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.
*
* <p>
* 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();
}

View file

@ -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<V> {
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<V> {
private final Map<Long, Page<V>> pages = new HashMap<>();
private static long getPageIndex(Address addr) {
return addr.getOffset() >> PAGE_BITS;
}
Entry<AddressRange, V> put(Entry<AddressRange, V> entry) {
AddressRange range = entry.getKey();
long indexMin = getPageIndex(range.getMinAddress());
Page<V> pageMin = pages.computeIfAbsent(indexMin, o -> new Page<>());
pageMin.put(entry);
long indexMax = getPageIndex(range.getMaxAddress());
if (indexMax == indexMin) {
return entry;
}
Page<V> pageMax = pages.computeIfAbsent(indexMax, o -> new Page<>());
return pageMax.put(entry);
}
boolean hasEntry(Address address, Predicate<V> predicate) {
Page<V> page = pages.get(getPageIndex(address));
if (page == null) {
return false;
}
return page.hasEntry(address, predicate);
}
}
private static class Page<V> {
static final Comparator<Entry<AddressRange, ?>> ENTRY_COMPARATOR = Page::compareEntries;
private final List<Entry<AddressRange, V>> entries = new ArrayList<>();
private static int compareEntries(Entry<AddressRange, ?> e1, Entry<AddressRange, ?> e2) {
return e1.getKey().getMinAddress().compareTo(e2.getKey().getMinAddress());
}
Entry<AddressRange, V> put(Entry<AddressRange, V> 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<V> predicate) {
for (Entry<AddressRange, V> 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<AddressSpace, Space<V>> spaces = new HashMap<>();
private boolean isEmpty = true;
public Entry<AddressRange, V> put(AddressRange range, V value) {
Space<V> space = spaces.computeIfAbsent(range.getAddressSpace(), s -> new Space<>());
Entry<AddressRange, V> entry = space.put(Map.entry(range, value));
isEmpty = false;
return entry;
}
public boolean hasEntry(Address address, Predicate<V> predicate) {
Space<V> 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;
}
}

View file

@ -329,6 +329,15 @@ public class PcodeExecutor<T> {
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<T> {
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<T> {
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<T> {
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);

View file

@ -130,6 +130,7 @@ public class PcodeFrame {
private final List<PcodeOp> code;
private final Map<Integer, String> 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
*
* <p>
* 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 <em>next</em> p-code op to be executed
*
@ -190,6 +204,7 @@ public class PcodeFrame {
* @return the value of the index <em>before</em> it was advanced
*/
public int advance() {
count++;
return index++;
}
@ -199,6 +214,7 @@ public class PcodeFrame {
* @return the value of the index <em>before</em> it was stepped back
*/
public int stepBack() {
count--;
return index--;
}

View file

@ -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<String> 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<String> 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<String> 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<String> 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));
}
}