GP-1881: Implement editable Repr column for Registers and Watches providers.

This commit is contained in:
Dan 2022-05-06 15:08:09 -04:00
parent 8e8b193ac1
commit dbe670bf85
12 changed files with 314 additions and 28 deletions

View file

@ -325,9 +325,9 @@
<H3><A name="toggle_update_while_running">Update While Running</A></H3>
<P>By default, events are passed to the Objects Viewer even while the target is running.
The resulting changes in the GUI may be distracting for some. To disable updates to the
Objects Viewer, toggle "Updates While Running" off.</P>
<P>By default, events are passed to the Objects Viewer even while the target is running. The
resulting changes in the GUI may be distracting for some. To disable updates to the Objects
Viewer, toggle "Updates While Running" off.</P>
<H2><A name="color"></A>Color Options</H2>

View file

@ -58,7 +58,8 @@
time on."</LI>
<LI>Representation - the value of the register as interpreted by its data type. If the value
is an address, double-clicking this field will navigate to it.</LI>
is an address, double-clicking this field will navigate to it. This field is user modifiable
whenever the Value column is modifiable and the selected data type provides an encoder.</LI>
</UL>
<H2>Actions</H2>

View file

@ -78,10 +78,10 @@
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. This
field is not yet user modifiable, even if the <B>Enable Edits</B> toggle is on. If the value
is an address, i.e., Type is a pointer, then double-clicking this cell will navigate the
primary dynamic listing, if possible.</LI>
<LI>Representation - the value of the watch as interpreted by the selected data type. If the
value is an address, i.e., Type is a pointer, then double-clicking this cell will navigate
the primary dynamic listing, if possible. This field is user-modifiable whenever the Value
column is modifiable and the selected data type provides an encoder.</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

View file

@ -62,8 +62,7 @@ import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.program.model.address.*;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.DataTypeConflictException;
import ghidra.program.model.data.*;
import ghidra.program.model.lang.*;
import ghidra.program.model.util.CodeUnitInsertionException;
import ghidra.trace.model.*;
@ -89,12 +88,16 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter
protected enum RegisterTableColumns
implements EnumeratedTableColumn<RegisterTableColumns, RegisterRow> {
FAV("Fav", Boolean.class, RegisterRow::isFavorite, RegisterRow::setFavorite, r -> true, SortDirection.DESCENDING),
FAV("Fav", Boolean.class, RegisterRow::isFavorite, RegisterRow::setFavorite, //
r -> true, SortDirection.DESCENDING),
NUMBER("#", Integer.class, RegisterRow::getNumber),
NAME("Name", String.class, RegisterRow::getName),
VALUE("Value", BigInteger.class, RegisterRow::getValue, RegisterRow::setValue, RegisterRow::isValueEditable, SortDirection.ASCENDING),
TYPE("Type", DataType.class, RegisterRow::getDataType, RegisterRow::setDataType, r -> true, SortDirection.ASCENDING),
REPR("Repr", String.class, RegisterRow::getRepresentation);
VALUE("Value", BigInteger.class, RegisterRow::getValue, RegisterRow::setValue, //
RegisterRow::isValueEditable, SortDirection.ASCENDING),
TYPE("Type", DataType.class, RegisterRow::getDataType, RegisterRow::setDataType, //
r -> true, SortDirection.ASCENDING),
REPR("Repr", String.class, RegisterRow::getRepresentation, RegisterRow::setRepresentation, //
RegisterRow::isRepresentationEditable, SortDirection.ASCENDING);
private final String header;
private final Function<RegisterRow, ?> getter;
@ -859,6 +862,35 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter
return data.getDataType();
}
void writeRegisterValueRepresentation(Register register, String representation) {
TraceData data = getRegisterData(register);
if (data == null) {
// isEditable should have been false
tool.setStatusInfo("Register has no data type", true);
return;
}
try {
RegisterValue rv = TraceRegisterUtils.encodeValueRepresentationHackPointer(
register, data, representation);
writeRegisterValue(rv);
}
catch (DataTypeEncodeException e) {
tool.setStatusInfo(e.getMessage(), true);
return;
}
}
boolean canWriteRegisterRepresentation(Register register) {
if (!canWriteRegister(register)) {
return false;
}
TraceData data = getRegisterData(register);
if (data == null) {
return false;
}
return data.getBaseDataType().isEncodable();
}
String getRegisterValueRepresentation(Register register) {
TraceData data = getRegisterData(register);
if (data == null) {

View file

@ -91,7 +91,13 @@ public class RegisterRow {
return provider.getRegisterDataType(register);
}
// TODO: setValueRepresentation. Requires support from data types.
public void setRepresentation(String representation) {
provider.writeRegisterValueRepresentation(register, representation);
}
public boolean isRepresentationEditable() {
return provider.canWriteRegisterRepresentation(register);
}
public String getRepresentation() {
return provider.getRegisterValueRepresentation(register);

View file

@ -86,9 +86,11 @@ 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, WatchRow::setRawValueString, WatchRow::isValueEditable),
VALUE("Value", String.class, WatchRow::getRawValueString, WatchRow::setRawValueString, //
WatchRow::isRawValueEditable),
TYPE("Type", DataType.class, WatchRow::getDataType, WatchRow::setDataType),
REPR("Repr", String.class, WatchRow::getValueString),
REPR("Repr", String.class, WatchRow::getValueString, WatchRow::setValueString, //
WatchRow::isValueEditable),
ERROR("Error", String.class, WatchRow::getErrorMessage);
private final String header;

View file

@ -33,6 +33,7 @@ import ghidra.pcode.exec.trace.TraceSleighUtils;
import ghidra.pcode.utils.Utils;
import ghidra.program.model.address.*;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.DataTypeEncodeException;
import ghidra.program.model.lang.Language;
import ghidra.program.model.mem.ByteMemBufferImpl;
import ghidra.program.model.mem.MemBuffer;
@ -143,6 +144,8 @@ public class WatchRow {
return dataType.getRepresentation(buffer, SettingsImpl.NO_SETTINGS, value.length);
}
// TODO: DataType settings
protected Object parseAsDataTypeObj() {
if (dataType == null || value == null) {
return null;
@ -373,7 +376,7 @@ public class WatchRow {
return valueObj;
}
public boolean isValueEditable() {
public boolean isRawValueEditable() {
if (!provider.isEditsEnabled()) {
return false;
}
@ -423,8 +426,13 @@ public class WatchRow {
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");
if (bytes.length > value.length) {
throw new IllegalArgumentException("Byte arrays cannot exceed length of variable");
}
if (bytes.length < value.length) {
byte[] fillOld = Arrays.copyOf(value, value.length);
System.arraycopy(bytes, 0, fillOld, 0, bytes.length);
bytes = fillOld;
}
DebuggerStateEditingService editingService = provider.editingService;
if (editingService == null) {
@ -438,6 +446,33 @@ public class WatchRow {
});
}
public void setValueString(String valueString) {
if (dataType == null || value == null) {
// isValueEditable should have been false
provider.getTool().setStatusInfo("Watch no value or no data type", true);
return;
}
try {
byte[] encoded = dataType.encodeRepresentation(valueString,
new ByteMemBufferImpl(address, value, language.isBigEndian()),
SettingsImpl.NO_SETTINGS, value.length);
setRawValueBytes(encoded);
}
catch (DataTypeEncodeException e) {
provider.getTool().setStatusInfo(e.getMessage(), true);
}
}
public boolean isValueEditable() {
if (!isRawValueEditable()) {
return false;
}
if (dataType == null) {
return false;
}
return dataType.isEncodable();
}
public int getValueLength() {
return value == null ? 0 : value.length;
}

View file

@ -19,6 +19,7 @@ import static ghidra.lifecycle.Unfinished.TODO;
import static org.junit.Assert.*;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.stream.Collectors;
@ -159,6 +160,11 @@ public class DebuggerRegistersProviderTest extends AbstractGhidraHeadedDebuggerG
row.setValue(new BigInteger(text, 16));
}
protected void setRowRepr(RegisterRow row, String repr) {
assertTrue(row.isRepresentationEditable());
row.setRepresentation(repr);
}
protected void assertRowValueEmpty(RegisterRow row) {
assertEquals(BigInteger.ZERO, row.getValue());
}
@ -393,6 +399,48 @@ public class DebuggerRegistersProviderTest extends AbstractGhidraHeadedDebuggerG
});
}
long encodeDouble(double value) {
ByteBuffer buf = ByteBuffer.allocate(Double.BYTES);
buf.putDouble(0, value);
return buf.getLong(0);
}
@Test
public void testModifyRepresentationEmu() throws Exception {
traceManager.openTrace(tb.trace);
TraceThread thread = addThread();
traceManager.activateThread(thread);
waitForSwing();
editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR);
assertTrue(registersProvider.actionEnableEdits.isEnabled());
performAction(registersProvider.actionEnableEdits);
addRegisterValues(thread);
waitForDomainObject(tb.trace);
TraceMemoryRegisterSpace regVals =
tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
RegisterRow row = findRegisterRow(r0);
assertFalse(row.isRepresentationEditable());
row.setDataType(DoubleDataType.dataType);
waitForDomainObject(tb.trace);
setRowRepr(row, "1234");
waitForSwing();
waitForPass(() -> {
long viewSnap = traceManager.getCurrent().getViewSnap();
assertTrue(DBTraceUtils.isScratch(viewSnap));
assertEquals(BigInteger.valueOf(encodeDouble(1234)),
regVals.getValue(viewSnap, r0).getUnsignedValue());
assertEquals(BigInteger.valueOf(encodeDouble(1234)), row.getValue());
});
}
// NOTE: Value modification only allowed on live target
@Test

View file

@ -270,17 +270,17 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI
WatchRow row = Unique.assertOne(watchesProvider.watchTableModel.getModelData());
row.setExpression(expression);
assertFalse(row.isValueEditable());
assertFalse(row.isRawValueEditable());
traceManager.openTrace(tb.trace);
traceManager.activateThread(thread);
editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_EMULATOR);
waitForSwing();
assertNoErr(row);
assertFalse(row.isValueEditable());
assertFalse(row.isRawValueEditable());
performAction(watchesProvider.actionEnableEdits);
assertEquals(expectWritable, row.isValueEditable());
assertEquals(expectWritable, row.isRawValueEditable());
}
@Test
@ -314,6 +314,12 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI
return row;
}
long encodeDouble(double value) {
ByteBuffer buf = ByteBuffer.allocate(Double.BYTES);
buf.putDouble(0, value);
return buf.getLong(0);
}
@Test
public void testEditRegisterEmu() {
WatchRow row = prepareTestEditEmu("r0");
@ -339,9 +345,33 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI
});
}
@Test
public void testEditRegisterRepresentationEmu() {
WatchRow row = prepareTestEditEmu("r0");
assertFalse(row.isValueEditable());
row.setDataType(DoubleDataType.dataType);
waitForSwing();
assertTrue(row.isValueEditable());
TraceMemoryRegisterSpace regVals =
tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
row.setValueString("1234");
waitForPass(() -> {
long viewSnap = traceManager.getCurrent().getViewSnap();
assertTrue(DBTraceUtils.isScratch(viewSnap));
assertEquals(BigInteger.valueOf(encodeDouble(1234)),
regVals.getValue(viewSnap, r0).getUnsignedValue());
assertEquals("0x4093480000000000", row.getRawValueString());
assertEquals("1234.0", row.getValueString());
});
}
@Test
public void testEditMemoryEmu() {
WatchRow row = prepareTestEditEmu("*:8 r0");
TraceMemoryOperations mem = tb.trace.getMemoryManager();
ByteBuffer buf = ByteBuffer.allocate(8);
@ -366,6 +396,55 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI
});
}
@Test
public void testEditMemoryRepresentationEmu() {
WatchRow row = prepareTestEditEmu("*:8 r0");
assertFalse(row.isValueEditable());
row.setDataType(DoubleDataType.dataType);
waitForSwing();
assertTrue(row.isValueEditable());
TraceMemoryOperations mem = tb.trace.getMemoryManager();
ByteBuffer buf = ByteBuffer.allocate(8);
row.setValueString("1234");
waitForPass(() -> {
long viewSnap = traceManager.getCurrent().getViewSnap();
assertTrue(DBTraceUtils.isScratch(viewSnap));
buf.clear();
mem.getBytes(viewSnap, tb.addr(0x00400000), buf);
buf.flip();
assertEquals(encodeDouble(1234), buf.getLong());
assertEquals("1234.0", row.getValueString());
});
}
@Test
public void testEditMemoryStringEmu() {
// Variable size must exceed that of desired string's bytes
WatchRow row = prepareTestEditEmu("*:16 r0");
assertFalse(row.isValueEditable());
row.setDataType(TerminatedStringDataType.dataType);
waitForSwing();
assertTrue(row.isValueEditable());
TraceMemoryOperations mem = tb.trace.getMemoryManager();
ByteBuffer buf = ByteBuffer.allocate(14);
row.setValueString("\"Hello, World!\"");
waitForPass(() -> {
long viewSnap = traceManager.getCurrent().getViewSnap();
assertTrue(DBTraceUtils.isScratch(viewSnap));
buf.clear();
mem.getBytes(viewSnap, tb.addr(0x00400000), buf);
buf.flip();
assertArrayEquals("Hello, World!\0".getBytes(), buf.array());
assertEquals("\"Hello, World!\"", row.getValueString());
});
}
protected WatchRow prepareTestEditTarget(String expression) throws Exception {
createTestModel();
mb.createTestProcessesAndThreads();
@ -422,7 +501,7 @@ public class DebuggerWatchesProviderTest extends AbstractGhidraHeadedDebuggerGUI
// Sanity check
assertFalse(recorder.isRegisterOnTarget(thread, r1));
assertFalse(row.isValueEditable());
assertFalse(row.isRawValueEditable());
row.setRawValueString("0x1234");
}

View file

@ -44,7 +44,21 @@ public class PatchStep implements Step {
protected String sleigh;
protected int hashCode;
public static String generateSleigh(Language language, Address address, byte[] data,
/**
* Generate a single line of Sleigh
*
* <p>
* Note that when length is greater than 8, this will generate constants which are too large for
* the Java implementation of Sleigh. Use {@link #generateSleigh(Language, Address, byte[])}
* instead to write the variable in chunks.
*
* @param language the target language
* @param address the (start) address of the variable
* @param data the bytes to write to the variable
* @param length the length of the variable
* @return the Sleigh code
*/
public static String generateSleighLine(Language language, Address address, byte[] data,
int length) {
BigInteger value = Utils.bytesToBigInteger(data, length, language.isBigEndian(), false);
if (address.isMemoryAddress()) {
@ -67,8 +81,34 @@ public class PatchStep implements Step {
return String.format("%s=0x%s", register, value.toString(16));
}
public static String generateSleigh(Language language, Address address, byte[] data) {
return generateSleigh(language, address, data, data.length);
/**
* Generate a single line of Sleigh
*
* @see #generateSleighLine(Language, Address, byte[], int)
*/
public static String generateSleighLine(Language language, Address address, byte[] data) {
return generateSleighLine(language, address, data, data.length);
}
/**
* Generate multiple lines of Sleigh, all to set a single variable
*
* @param language the target language
* @param address the (start) address of the variable
* @param data the bytes to write to the variable
* @return the lines of Sleigh code
*/
public static List<String> generateSleigh(Language language, Address address, byte[] data) {
List<String> result = new ArrayList<>();
generateSleigh(result, language, address, data);
return result;
}
protected static void generateSleigh(List<String> result, Language language, Address address,
byte[] data) {
SemisparseByteArray array = new SemisparseByteArray(); // TODO: Seems heavy-handed
array.putData(address.getOffset(), data);
generateSleigh(result, language, address.getAddressSpace(), array);
}
protected static List<String> generateSleigh(Language language,
@ -102,7 +142,7 @@ public class PatchStep implements Step {
Address min = chunk.getMinAddress();
int length = (int) chunk.getLength();
array.getData(min.getOffset(), data, 0, length);
result.add(generateSleigh(language, min, data, length));
result.add(generateSleighLine(language, min, data, length));
}
}
}

View file

@ -494,6 +494,7 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
/**
* Returns the equivalent of executing this schedule then performing a given patch
*
* @param thread the thread context for the patch; cannot be null
* @param sleigh a single line of sleigh, excluding the terminating semicolon.
* @return the resulting schedule
*/
@ -509,4 +510,27 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
ticks.coalescePatches(thread.getTrace().getBaseLanguage());
return new TraceSchedule(snap, ticks, new Sequence());
}
/**
* Returns the equivalent of executing this schedule then performing the given patches
*
* @param thread the thread context for the patch; cannot be null
* @param sleigh the lines of sleigh, excluding the terminating semicolons.
* @return the resulting schedule
*/
public TraceSchedule patched(TraceThread thread, List<String> sleigh) {
if (!this.pSteps.isNop()) {
Sequence pTicks = this.pSteps.clone();
for (String line : sleigh) {
pTicks.advance(new PatchStep(thread.getKey(), line));
}
pTicks.coalescePatches(thread.getTrace().getBaseLanguage());
return new TraceSchedule(snap, steps.clone(), pTicks);
}
Sequence ticks = this.steps.clone();
for (String line : sleigh) {
ticks.advance(new PatchStep(thread.getKey(), line));
}
return new TraceSchedule(snap, ticks, new Sequence());
}
}

View file

@ -15,12 +15,14 @@
*/
package ghidra.trace.util;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.function.BiConsumer;
import org.apache.commons.lang3.ArrayUtils;
import ghidra.pcode.utils.Utils;
import ghidra.program.model.address.*;
import ghidra.program.model.data.*;
import ghidra.program.model.lang.Register;
@ -122,6 +124,23 @@ public enum TraceRegisterUtils {
return addr.toString();
}
public static RegisterValue encodeValueRepresentationHackPointer(Register register,
TraceData data, String representation) throws DataTypeEncodeException {
DataType dataType = data.getBaseDataType();
if (data.getValueClass() != Address.class) {
byte[] bytes =
dataType.encodeRepresentation(representation, data, data, data.getLength());
BigInteger value = Utils.bytesToBigInteger(bytes, register.getMinimumByteSize(),
register.isBigEndian(), false);
return new RegisterValue(register, value);
}
Address addr = data.getTrace().getBaseAddressFactory().getAddress(representation);
if (addr == null) {
throw new DataTypeEncodeException("Invalid address", representation, dataType);
}
return new RegisterValue(register, addr.getOffsetAsBigInteger());
}
public static RegisterValue combineWithTraceBaseRegisterValue(RegisterValue rv, long snap,
TraceMemoryRegisterSpace regs, boolean requireKnown) {
Register reg = rv.getRegister();