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.

Snapshot Window

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html index d82b143c25..5247ccc4f5 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html @@ -61,14 +61,17 @@ possible.
  • Value - the raw bytes of the watched buffer. If the expression is a register, then this - is its hexadecimal value. If the value has changed since the last navigation event, this cell - is rendered in red.
  • + is its hexadecimal value. This field is user modifiable when the Enable Edits toggle + is on. Changes are sent to the target if the trace is live and "at the present." If the value + has changed since the last navigation event, this cell is rendered in red.
  • Type - the user-modifiable type of the watch. Note the type is not marked up in the trace. Clicking the Apply Data Type action will apply it to the current trace, if possible.
  • -
  • Representation - the value of the watch as interpreted by the selected data type.
  • +
  • Representation - the value of the watch as interpreted by the selected data type. This + field is not yet user modifiable, even if the Enable Edits toggle is on.
  • 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")); + } }