diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/images/DebuggerModulesPlugin.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/images/DebuggerModulesPlugin.png
index c30378df45..a0b9768c89 100644
Binary files a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/images/DebuggerModulesPlugin.png and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/images/DebuggerModulesPlugin.png differ
diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html
index 6c9df48119..8b59011a9e 100644
--- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html
+++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html
@@ -93,8 +93,8 @@
This toggle is a write protector for live registers. To modify live register values, this
toggle must be enabled, and the trace must be live and "at the present." Note that editing
- recorded historical values is not permitted via the UI, regardless of this toggle, but can be
- accomplished via scripts.
+ recorded historical values is not permitted, regardless of this toggle, but can be accomplished
+ via watches or scripts.
Error - if an error occurs during compilation or evaluation of the expression, that error
is rendered here. Double-clicking the row will display the stack trace. Note that errors
@@ -118,6 +121,14 @@
This action is available when at least one watch is selected. It removes those watches.
+ Enable Edits
+
+ This toggle is a write protector for recorded and/or live values. To modify a watch's value,
+ this toggle must be enabled. Editing a value when the trace is live and "at the present" will
+ cause the value to be modified on the target. Editing historical and/or emulated values is
+ permitted, but it has no effect on the target. Note that only the raw "Value" column can be
+ edited directly. The "Repr" column cannot be edited, yet.
+
Tool Options: Colors
The watch window uses colors to hint about changes in and freshness of displayed values.
diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/images/DebuggerWatchesPlugin.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/images/DebuggerWatchesPlugin.png
index c9b63a6770..cc375abf7a 100644
Binary files a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/images/DebuggerWatchesPlugin.png and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/images/DebuggerWatchesPlugin.png differ
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java
index a6bf9b40b3..67047d5f53 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java
@@ -895,10 +895,10 @@ public interface DebuggerResources {
}
}
- interface EnableRegisterEditsAction {
+ interface EnableEditsAction {
String NAME = "Enable Edits";
- String DESCRIPTION = "Enable editing of recorded register values";
- String GROUP = "yyyy";
+ String DESCRIPTION = "Enable editing of recorded or live values";
+ String GROUP = "yyyy2";
Icon ICON = ResourceManager.loadImage("images/editbytes.gif");
String HELP_ANCHOR = "enable_edits";
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java
index 06aa6b86fc..877baba0f2 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java
@@ -604,7 +604,7 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter
.onAction(c -> createSnapshotActivated())
.buildAndInstallLocal(this);
}
- actionEnableEdits = DebuggerResources.EnableRegisterEditsAction.builder(plugin)
+ actionEnableEdits = DebuggerResources.EnableEditsAction.builder(plugin)
.enabledWhen(c -> current.getThread() != null)
.onAction(c -> {
})
@@ -758,12 +758,7 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter
if (!computeEditsEnabled()) {
return false;
}
- Collection onTarget =
- current.getRecorder().getRegisterMapper(current.getThread()).getRegistersOnTarget();
- if (!onTarget.contains(register) && !onTarget.contains(register.getBaseRegister())) {
- return false;
- }
- return true;
+ return current.getRecorder().isRegisterOnTarget(current.getThread(), register);
}
BigInteger getRegisterValue(Register register) {
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProvider.java
index 3498d7301a..87795d1c37 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProvider.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProvider.java
@@ -21,8 +21,7 @@ import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
-import java.util.function.BiConsumer;
-import java.util.function.Function;
+import java.util.function.*;
import javax.swing.*;
import javax.swing.table.TableColumn;
@@ -31,6 +30,7 @@ import javax.swing.table.TableColumnModel;
import docking.ActionContext;
import docking.WindowPosition;
import docking.action.DockingAction;
+import docking.action.ToggleDockingAction;
import docking.widgets.table.*;
import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
@@ -75,7 +75,7 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter {
protected enum WatchTableColumns implements EnumeratedTableColumn {
EXPRESSION("Expression", String.class, WatchRow::getExpression, WatchRow::setExpression),
ADDRESS("Address", Address.class, WatchRow::getAddress),
- VALUE("Value", String.class, WatchRow::getRawValueString),
+ VALUE("Value", String.class, WatchRow::getRawValueString, WatchRow::setRawValueString, WatchRow::isValueEditable),
TYPE("Type", DataType.class, WatchRow::getDataType, WatchRow::setDataType),
REPR("Repr", String.class, WatchRow::getValueString),
ERROR("Error", String.class, WatchRow::getErrorMessage);
@@ -83,19 +83,26 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter {
private final String header;
private final Function getter;
private final BiConsumer setter;
+ private final Predicate editable;
private final Class> cls;
@SuppressWarnings("unchecked")
WatchTableColumns(String header, Class cls, Function getter,
- BiConsumer setter) {
+ BiConsumer setter, Predicate editable) {
this.header = header;
this.cls = cls;
this.getter = getter;
this.setter = (BiConsumer) setter;
+ this.editable = editable;
+ }
+
+ WatchTableColumns(String header, Class cls, Function getter,
+ BiConsumer setter) {
+ this(header, cls, getter, setter, null);
}
WatchTableColumns(String header, Class cls, Function getter) {
- this(header, cls, getter, null);
+ this(header, cls, getter, null, null);
}
@Override
@@ -120,7 +127,7 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter {
@Override
public boolean isEditable(WatchRow row) {
- return setter != null;
+ return setter != null && (editable == null || editable.test(row));
}
}
@@ -267,6 +274,7 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter {
protected GhidraTable watchTable;
protected GhidraTableFilterPanel watchFilterPanel;
+ ToggleDockingAction actionEnableEdits;
DockingAction actionApplyDataType;
DockingAction actionSelectRange;
DockingAction actionSelectAllReads;
@@ -360,6 +368,11 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter {
}
protected void createActions() {
+ actionEnableEdits = DebuggerResources.EnableEditsAction.builder(plugin)
+ .enabledWhen(c -> current.getTrace() != null)
+ .onAction(c -> {
+ })
+ .buildAndInstallLocal(this);
actionApplyDataType = ApplyDataTypeAction.builder(plugin)
.withContext(DebuggerWatchActionContext.class)
.enabledWhen(ctx -> current.getTrace() != null && selHasDataType(ctx))
@@ -622,4 +635,8 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter {
}
watchTableModel.addAll(rows);
}
+
+ public boolean isEditsEnabled() {
+ return actionEnableEdits.isSelected();
+ }
}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/WatchRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/WatchRow.java
index 16f79849ed..beab74594d 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/WatchRow.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/WatchRow.java
@@ -16,6 +16,7 @@
package ghidra.app.plugin.core.debug.gui.watch;
import java.math.BigInteger;
+import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Objects;
@@ -38,12 +39,15 @@ import ghidra.trace.model.Trace;
import ghidra.trace.model.memory.TraceMemorySpace;
import ghidra.trace.model.memory.TraceMemoryState;
import ghidra.trace.model.thread.TraceThread;
-import ghidra.util.NumericUtilities;
-import ghidra.util.Swing;
+import ghidra.util.*;
+import ghidra.util.database.UndoableTransaction;
public class WatchRow {
+ public static final int TRUNCATE_BYTES_LENGTH = 64;
+
private final DebuggerWatchesProvider provider;
private Trace trace;
+ private DebuggerCoordinates coordinates;
private SleighLanguage language;
private PcodeExecutor> executorWithState;
private ReadDepsPcodeExecutor executorWithAddress;
@@ -218,6 +222,7 @@ public class WatchRow {
// NB. Caller has already verified coordinates actually changed
prevValue = value;
trace = coordinates.getTrace();
+ this.coordinates = coordinates;
updateType();
if (trace == null) {
blank();
@@ -327,8 +332,12 @@ public class WatchRow {
Utils.bytesToBigInteger(value, value.length, language.isBigEndian(), false);
return "0x" + asBigInt.toString(16);
}
- if (value.length > 20) {
- return "{ " + NumericUtilities.convertBytesToString(value, 0, 20, " ") + " ... }";
+ if (value.length > TRUNCATE_BYTES_LENGTH) {
+ // TODO: I'd like this not to affect the actual value, just the display
+ // esp., since this will be the "value" when starting to edit.
+ return "{ " +
+ NumericUtilities.convertBytesToString(value, 0, TRUNCATE_BYTES_LENGTH, " ") +
+ " ... }";
}
return "{ " + NumericUtilities.convertBytesToString(value, " ") + " }";
}
@@ -345,6 +354,79 @@ public class WatchRow {
return valueString;
}
+ public boolean isValueEditable() {
+ return address != null && provider.isEditsEnabled();
+ }
+
+ public void setRawValueString(String valueString) {
+ valueString = valueString.trim();
+ if (valueString.startsWith("{")) {
+ if (!valueString.endsWith("}")) {
+ throw new NumberFormatException("Byte array values must be hex enclosed in {}");
+ }
+
+ setRawValueBytesString(valueString.substring(1, valueString.length() - 1));
+ return;
+ }
+
+ setRawValueIntString(valueString);
+ }
+
+ public void setRawValueBytesString(String bytesString) {
+ setRawValueBytes(NumericUtilities.convertStringToBytes(bytesString));
+ }
+
+ public void setRawValueIntString(String intString) {
+ intString = intString.trim();
+ final BigInteger val;
+ if (intString.startsWith("0x")) {
+ val = new BigInteger(intString.substring(2), 16);
+ }
+ else {
+ val = new BigInteger(intString, 10);
+ }
+ setRawValueBytes(
+ Utils.bigIntegerToBytes(val, value.length, trace.getBaseLanguage().isBigEndian()));
+ }
+
+ public void setRawValueBytes(byte[] bytes) {
+ if (address == null) {
+ throw new IllegalStateException("Cannot write to watch variable without an address");
+ }
+ if (bytes.length != value.length) {
+ throw new IllegalArgumentException("Byte array values must match length of variable");
+ }
+
+ // Allow writes to unmappable registers to fall through to trace
+ // However, attempts to write "weird" register addresses is forbidden
+ if (coordinates.isAliveAndPresent() && coordinates.getRecorder()
+ .isVariableOnTarget(coordinates.getThread(), address, bytes.length)) {
+ coordinates.getRecorder()
+ .writeVariable(coordinates.getThread(), coordinates.getFrame(), address, bytes)
+ .exceptionally(ex -> {
+ Msg.showError(this, null, "Write Failed",
+ "Could not modify watch value (on target)", ex);
+ return null;
+ });
+ // NB: if successful, recorder will write to trace
+ return;
+ }
+
+ try (UndoableTransaction tid =
+ UndoableTransaction.start(trace, "Write watch at " + address, true)) {
+ final TraceMemorySpace space;
+ if (address.isRegisterAddress()) {
+ space = trace.getMemoryManager()
+ .getMemoryRegisterSpace(coordinates.getThread(), coordinates.getFrame(),
+ true);
+ }
+ else {
+ space = trace.getMemoryManager().getMemorySpace(address.getAddressSpace(), true);
+ }
+ space.putBytes(coordinates.getViewSnap(), address, ByteBuffer.wrap(bytes));
+ }
+ }
+
public int getValueLength() {
return value == null ? 0 : value.length;
}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java
index d560555f43..1f587f7a55 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java
@@ -26,20 +26,22 @@ import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.lifecycle.Internal;
+import ghidra.pcode.utils.Utils;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSetView;
-import ghidra.program.model.lang.Register;
-import ghidra.program.model.lang.RegisterValue;
+import ghidra.program.model.lang.*;
import ghidra.trace.model.Trace;
import ghidra.trace.model.breakpoint.TraceBreakpoint;
import ghidra.trace.model.breakpoint.TraceBreakpointKind;
import ghidra.trace.model.memory.TraceMemoryRegion;
+import ghidra.trace.model.memory.TraceMemoryRegisterSpace;
import ghidra.trace.model.modules.TraceModule;
import ghidra.trace.model.modules.TraceSection;
import ghidra.trace.model.stack.TraceStackFrame;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.TraceTimeManager;
+import ghidra.trace.util.TraceRegisterUtils;
import ghidra.util.task.TaskMonitor;
/**
@@ -331,6 +333,86 @@ public interface TraceRecorder {
CompletableFuture> captureProcessMemory(AddressSetView selection,
TaskMonitor monitor);
+ /**
+ * Write a variable (memory or register) of the given thread or the process
+ *
+ *
+ * This is a convenience for writing target memory or registers, based on address. If the given
+ * address represents a register, this will attempt to map it to a register and write it in the
+ * given thread and frame. If the address is in memory, it will simply delegate to
+ * {@link #writeProcessMemory(Address, byte[])}.
+ *
+ * @param thread the thread. Ignored (may be null) if address is in memory
+ * @param frameLevel the frame, usually 0. Ignored if address is in memory
+ * @param address the starting address
+ * @param data the value to write
+ * @return a future which completes when the write is complete
+ */
+ default CompletableFuture writeVariable(TraceThread thread, int frameLevel,
+ Address address, byte[] data) {
+ if (address.isMemoryAddress()) {
+ return writeProcessMemory(address, data);
+ }
+ if (address.isRegisterAddress()) {
+ Language lang = getTrace().getBaseLanguage();
+ Register register = lang.getRegister(address, data.length);
+ if (register == null) {
+ throw new IllegalArgumentException(
+ "Cannot identify the (single) register to write: " + address);
+ }
+
+ RegisterValue rv = new RegisterValue(register,
+ Utils.bytesToBigInteger(data, data.length, lang.isBigEndian(), false));
+ TraceMemoryRegisterSpace regs =
+ getTrace().getMemoryManager().getMemoryRegisterSpace(thread, frameLevel, false);
+ rv = TraceRegisterUtils.combineWithTraceBaseRegisterValue(rv, getSnap(), regs, true);
+ return writeThreadRegisters(thread, frameLevel, Map.of(rv.getRegister(), rv));
+ }
+ throw new IllegalArgumentException("Address is not in a recognized space: " + address);
+ }
+
+ /**
+ * Check if the given register exists on target (is mappable) for the given thread
+ *
+ * @param thread the thread whose registers to examine
+ * @param register the register to check
+ * @return true if the given register is known for the given thread on target
+ */
+ default boolean isRegisterOnTarget(TraceThread thread, Register register) {
+ Collection onTarget = getRegisterMapper(thread).getRegistersOnTarget();
+ return onTarget.contains(register) || onTarget.contains(register.getBaseRegister());
+ }
+
+ /**
+ * Check if the given trace address exists in target memory
+ *
+ * @param address the address to check
+ * @return true if the given trace address can be mapped to the target's memory
+ */
+ default boolean isMemoryOnTarget(Address address) {
+ return getMemoryMapper().traceToTarget(address) != null;
+ }
+
+ /**
+ * Check if a given variable (register or memory) exists on target
+ *
+ * @param thread if a register, the thread whose registers to examine
+ * @param address the address of the variable
+ * @param size the size of the variable. Ignored for memory
+ * @return true if the variable can be mapped to the target
+ */
+ default boolean isVariableOnTarget(TraceThread thread, Address address, int size) {
+ if (address.isMemoryAddress()) {
+ return isMemoryOnTarget(address);
+ }
+ Register register = getTrace().getBaseLanguage().getRegister(address, size);
+ if (register == null) {
+ throw new IllegalArgumentException("Cannot identify the (single) register: " + address);
+ }
+
+ return isRegisterOnTarget(thread, register);
+ }
+
/**
* Capture the data types of a target's module.
*
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecutorState.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecutorState.java
index e513b0040f..b3b4b0b9d5 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecutorState.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecutorState.java
@@ -25,10 +25,8 @@ import ghidra.pcode.exec.trace.TraceMemoryStatePcodeExecutorStatePiece;
import ghidra.pcode.utils.Utils;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.*;
-import ghidra.trace.model.memory.TraceMemoryRegisterSpace;
import ghidra.trace.model.memory.TraceMemoryState;
import ghidra.trace.model.thread.TraceThread;
-import ghidra.trace.util.TraceRegisterUtils;
import ghidra.util.task.TaskMonitor;
public class TraceRecorderAsyncPcodeExecutorState
@@ -48,28 +46,7 @@ public class TraceRecorderAsyncPcodeExecutorState
protected CompletableFuture> doSetTargetVar(AddressSpace space, long offset, int size,
boolean truncateAddressableUnit, byte[] val) {
- if (space.isMemorySpace()) {
- return recorder.writeProcessMemory(space.getAddress(offset), val);
- }
- assert space.isRegisterSpace();
-
- Language lang = recorder.getTrace().getBaseLanguage();
- Register register = lang.getRegister(space, offset, size);
- if (register == null) {
- // TODO: Is this too restrictive? I can't imagine any code producing such nonsense
- throw new IllegalArgumentException(
- "write to register space must be to one register");
- }
-
- RegisterValue rv = new RegisterValue(register, Utils.bytesToBigInteger(
- val, size, recorder.getTrace().getBaseLanguage().isBigEndian(), false));
- TraceMemoryRegisterSpace regs = recorder.getTrace()
- .getMemoryManager()
- .getMemoryRegisterSpace(traceState.getThread(), false);
- rv = TraceRegisterUtils.combineWithTraceBaseRegisterValue(rv, traceState.getSnap(),
- regs, true);
- return recorder.writeThreadRegisters(traceState.getThread(), traceState.getFrame(),
- Map.of(rv.getRegister(), rv));
+ return recorder.writeVariable(traceState.getThread(), 0, space.getAddress(offset), val);
}
protected byte[] knitFromResults(NavigableMap map, Address addr, int size) {
diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProviderTest.java
index 8b09279d48..05976b2654 100644
--- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProviderTest.java
+++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProviderTest.java
@@ -19,6 +19,7 @@ import static org.junit.Assert.*;
import java.math.BigInteger;
import java.nio.ByteBuffer;
+import java.util.List;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.junit.*;
@@ -27,6 +28,7 @@ import generic.Unique;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin;
import ghidra.app.services.TraceRecorder;
+import ghidra.async.AsyncTestUtils;
import ghidra.dbg.model.TestTargetRegisterBankInThread;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRangeImpl;
@@ -35,13 +37,15 @@ import ghidra.program.model.data.LongLongDataType;
import ghidra.program.model.lang.Register;
import ghidra.program.model.lang.RegisterValue;
import ghidra.trace.model.Trace;
+import ghidra.trace.model.memory.TraceMemoryOperations;
import ghidra.trace.model.memory.TraceMemoryRegisterSpace;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.util.TraceRegisterUtils;
import ghidra.util.Msg;
import ghidra.util.database.UndoableTransaction;
-public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
+public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUITest
+ implements AsyncTestUtils {
protected static void assertNoErr(WatchRow row) {
Throwable error = row.getError();
@@ -55,8 +59,12 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI
protected DebuggerListingPlugin listingPlugin;
protected Register r0;
+ protected Register r1;
protected TraceThread thread;
+ protected TestTargetRegisterBankInThread bank;
+ protected TraceRecorder recorder;
+
@Before
public void setUpWatchesProviderTest() throws Exception {
watchesPlugin = addPlugin(tool, DebuggerWatchesPlugin.class);
@@ -65,6 +73,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI
createTrace();
r0 = tb.language.getRegister("r0");
+ r1 = tb.language.getRegister("r1");
try (UndoableTransaction tid = tb.startTransaction()) {
thread = tb.getOrAddThread("Thread1", 0);
}
@@ -189,7 +198,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI
public void testLiveCausesReads() throws Exception {
createTestModel();
mb.createTestProcessesAndThreads();
- TestTargetRegisterBankInThread bank = mb.testThread1.addRegisterBank();
+ bank = mb.testThread1.addRegisterBank();
// Write before we record, and verify trace has not recorded it before setting watch
mb.testProcess1.regs.addRegistersFromLanguage(tb.language, Register::isBaseRegister);
@@ -198,7 +207,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI
mb.testProcess1.addRegion(".text", mb.rng(0x00400000, 0x00401000), "rx");
mb.testProcess1.memory.writeMemory(mb.addr(0x00400000), tb.arr(1, 2, 3, 4));
- TraceRecorder recorder = modelService.recordTarget(mb.testProcess1,
+ recorder = modelService.recordTarget(mb.testProcess1,
new TestDebuggerTargetTraceMapper(mb.testProcess1));
Trace trace = recorder.getTrace();
TraceThread thread = waitForValue(() -> recorder.getTraceThread(mb.testThread1));
@@ -231,4 +240,157 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI
});
assertNoErr(row);
}
+
+ protected void runTestDeadIsEditable(String expression, boolean expectWritable) {
+ setRegisterValues(thread);
+
+ performAction(watchesProvider.actionAdd);
+ WatchRow row = Unique.assertOne(watchesProvider.watchTableModel.getModelData());
+ row.setExpression(expression);
+
+ assertFalse(row.isValueEditable());
+ traceManager.openTrace(tb.trace);
+ traceManager.activateThread(thread);
+ waitForSwing();
+
+ assertNoErr(row);
+ assertFalse(row.isValueEditable());
+
+ performAction(watchesProvider.actionEnableEdits);
+ assertEquals(expectWritable, row.isValueEditable());
+ }
+
+ @Test
+ public void testDeadIsRegisterEditable() {
+ runTestDeadIsEditable("r0", true);
+ }
+
+ @Test
+ public void testDeadIsUniqueEditable() {
+ runTestDeadIsEditable("r0 + 8", false);
+ }
+
+ @Test
+ public void testDeadIsMemoryEditable() {
+ runTestDeadIsEditable("*:8 r0", true);
+ }
+
+ protected WatchRow prepareTestDeadEdit(String expression) {
+ setRegisterValues(thread);
+
+ performAction(watchesProvider.actionAdd);
+ WatchRow row = Unique.assertOne(watchesProvider.watchTableModel.getModelData());
+ row.setExpression("r0");
+
+ traceManager.openTrace(tb.trace);
+ traceManager.activateThread(thread);
+ performAction(watchesProvider.actionEnableEdits);
+
+ return row;
+ }
+
+ @Test
+ public void testDeadEditRegister() {
+ WatchRow row = prepareTestDeadEdit("r0");
+
+ row.setRawValueString("0x1234");
+ waitForSwing();
+
+ TraceMemoryRegisterSpace regVals =
+ tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
+ assertEquals(BigInteger.valueOf(0x1234), regVals.getValue(0, r0).getUnsignedValue());
+
+ row.setRawValueString("1234");
+ waitForSwing();
+
+ assertEquals(BigInteger.valueOf(1234), regVals.getValue(0, r0).getUnsignedValue());
+ }
+
+ @Test
+ public void testDeadEditMemory() {
+ WatchRow row = prepareTestDeadEdit("*:8 r0");
+
+ row.setRawValueString("0x1234");
+ waitForSwing();
+
+ TraceMemoryOperations mem = tb.trace.getMemoryManager();
+ ByteBuffer buf = ByteBuffer.allocate(8);
+ mem.getBytes(0, tb.addr(0x00400000), buf);
+ buf.flip();
+ assertEquals(0x1234, buf.getLong());
+
+ row.setRawValueString("{ 12 34 56 78 9a bc de f0 }");
+ waitForSwing();
+ buf.clear();
+ mem.getBytes(0, tb.addr(0x00400000), buf);
+ buf.flip();
+ assertEquals(0x123456789abcdef0L, buf.getLong());
+ }
+
+ protected WatchRow prepareTestLiveEdit(String expression) throws Exception {
+ createTestModel();
+ mb.createTestProcessesAndThreads();
+ bank = mb.testThread1.addRegisterBank();
+
+ mb.testProcess1.regs.addRegistersFromLanguage(tb.language,
+ r -> r != r1 && r.isBaseRegister());
+ bank.writeRegister("r0", tb.arr(0, 0, 0, 0, 0, 0x40, 0, 0));
+ mb.testProcess1.addRegion(".text", mb.rng(0x00400000, 0x00401000), "rx");
+
+ recorder = modelService.recordTarget(mb.testProcess1,
+ new TestDebuggerTargetTraceMapper(mb.testProcess1));
+ Trace trace = recorder.getTrace();
+ TraceThread thread = waitForValue(() -> recorder.getTraceThread(mb.testThread1));
+
+ traceManager.openTrace(trace);
+ traceManager.activateThread(thread);
+ waitForSwing();
+
+ performAction(watchesProvider.actionAdd);
+ WatchRow row = Unique.assertOne(watchesProvider.watchTableModel.getModelData());
+ row.setExpression(expression);
+ performAction(watchesProvider.actionEnableEdits);
+
+ return row;
+ }
+
+ @Test
+ public void testLiveEditRegister() throws Throwable {
+ WatchRow row = prepareTestLiveEdit("r0");
+
+ row.setRawValueString("0x1234");
+ retryVoid(() -> {
+ assertArrayEquals(tb.arr(0, 0, 0, 0, 0, 0, 0x12, 0x34), bank.regVals.get("r0"));
+ }, List.of(AssertionError.class));
+ }
+
+ @Test
+ public void testLiveEditMemory() throws Throwable {
+ WatchRow row = prepareTestLiveEdit("*:8 r0");
+
+ row.setRawValueString("0x1234");
+ retryVoid(() -> {
+ assertArrayEquals(tb.arr(0, 0, 0, 0, 0, 0, 0x12, 0x34),
+ waitOn(mb.testProcess1.memory.readMemory(tb.addr(0x00400000), 8)));
+ }, List.of(AssertionError.class));
+ }
+
+ @Test
+ public void testLiveEditNonMappableRegister() throws Throwable {
+ WatchRow row = prepareTestLiveEdit("r1");
+ TraceThread thread = recorder.getTraceThread(mb.testThread1);
+
+ // Sanity check
+ assertFalse(recorder.isRegisterOnTarget(thread, r1));
+
+ row.setRawValueString("0x1234");
+ waitForSwing();
+
+ TraceMemoryRegisterSpace regs =
+ recorder.getTrace().getMemoryManager().getMemoryRegisterSpace(thread, false);
+ assertEquals(BigInteger.valueOf(0x1234),
+ regs.getValue(recorder.getSnap(), r1).getUnsignedValue());
+
+ assertFalse(bank.regVals.containsKey("r1"));
+ }
}