mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-23 04:32:12 +00:00
Merge remote-tracking branch
'origin/GP-1264_Dan_writeViaWatches--SQUASHED' into patch (Closes #2866)
This commit is contained in:
commit
aa38095f77
Binary file not shown.
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
@ -93,8 +93,8 @@
|
||||
|
||||
<P>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.</P>
|
||||
recorded historical values is not permitted, regardless of this toggle, but can be accomplished
|
||||
via watches or scripts.</P>
|
||||
|
||||
<H3><A name="snapshot_window"></A>Snapshot Window</H3>
|
||||
|
||||
|
@ -61,14 +61,17 @@
|
||||
possible.</LI>
|
||||
|
||||
<LI>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 <FONT color="red">red</FONT>.</LI>
|
||||
is its hexadecimal value. This field is user modifiable when the <B>Enable Edits</B> 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 <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
|
||||
possible.</LI>
|
||||
|
||||
<LI>Representation - the value of the watch as interpreted by the selected data type.</LI>
|
||||
<LI>Representation - the value of the watch as interpreted by the selected data type. This
|
||||
field is not yet user modifiable, even if the <B>Enable Edits</B> toggle is on.</LI>
|
||||
|
||||
<LI>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 @@
|
||||
|
||||
<P>This action is available when at least one watch is selected. It removes those watches.</P>
|
||||
|
||||
<H3><A name="enable_edits"></A>Enable Edits</H3>
|
||||
|
||||
<P>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.</P>
|
||||
|
||||
<H2><A name="colors"></A>Tool Options: Colors</H2>
|
||||
|
||||
<P>The watch window uses colors to hint about changes in and freshness of displayed values.
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
@ -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";
|
||||
|
||||
|
@ -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<Register> 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) {
|
||||
|
@ -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<WatchTableColumns, WatchRow> {
|
||||
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<WatchRow, ?> getter;
|
||||
private final BiConsumer<WatchRow, Object> setter;
|
||||
private final Predicate<WatchRow> editable;
|
||||
private final Class<?> cls;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<T> WatchTableColumns(String header, Class<T> cls, Function<WatchRow, T> getter,
|
||||
BiConsumer<WatchRow, T> setter) {
|
||||
BiConsumer<WatchRow, T> setter, Predicate<WatchRow> editable) {
|
||||
this.header = header;
|
||||
this.cls = cls;
|
||||
this.getter = getter;
|
||||
this.setter = (BiConsumer<WatchRow, Object>) setter;
|
||||
this.editable = editable;
|
||||
}
|
||||
|
||||
<T> WatchTableColumns(String header, Class<T> cls, Function<WatchRow, T> getter,
|
||||
BiConsumer<WatchRow, T> setter) {
|
||||
this(header, cls, getter, setter, null);
|
||||
}
|
||||
|
||||
<T> WatchTableColumns(String header, Class<T> cls, Function<WatchRow, T> 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<WatchRow> 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();
|
||||
}
|
||||
}
|
||||
|
@ -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<Pair<byte[], TraceMemoryState>> 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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
@ -336,6 +338,86 @@ public interface TraceRecorder {
|
||||
CompletableFuture<NavigableMap<Address, byte[]>> captureProcessMemory(AddressSetView selection,
|
||||
TaskMonitor monitor, boolean toMap);
|
||||
|
||||
/**
|
||||
* Write a variable (memory or register) of the given thread or the process
|
||||
*
|
||||
* <p>
|
||||
* 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<Void> 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<Register> 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.
|
||||
*
|
||||
|
@ -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<Address, byte[]> map, Address addr, int size) {
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user