Watches refer to expressions which are evaluated each pause in order to monitor the value of
+ variables in the target machine state. The watch variables are expressed in Sleigh and
+ evaluated in the current thread's and trace's context at the current point in time. If the
+ current trace is live and at the present, then the target state is read and recorded as
+ necessary. The watch can be assigned a data type so that the raw data is rendered in a
+ meaningful way. When applicable, that data type can optionally be applied to the trace
+ database. Some metadata about the watch is also given, e.g., the address of the value.
+
+
Examples
+
+
For those less familar with Sleigh, here are some example expressions:
+
+
+
*:4 (RSP+8): Display 4 bytes of [ram] starting 8 bytes after the offset
+ given by register RSP.
+
+
*:4 0x7fff0004:8: Display 4 bytes starting at ram:7fff0004. The extraneous,
+ but required, size specifier on constant derefs is a known issue. Just use the target's
+ pointer size in bytes.
+
+
*:8 RSP: Display 8 bytes of [ram] starting at the offset given by register
+ RSP.
+
+
RSP: Display the value of register RSP.
+
+
+
Table Columns
+
+
The table displays and allows modification of each watch. It has the following columns:
+
+
+
Expression - the user-modifiable Sleigh expression defining this watch.
+
+
Address - when evaluation succeeds, the address of the watch's value. This field is
+ really only meaningful when the outermost operator of the expression is a memory dereference.
+ Double-clicking a row will navigate the primary dynamic listing to this address, if
+ 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.
+
+
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 the selected data type.
+
+
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
+ during evaluation can be a very common occurrence, especially as contexts change, and should
+ not cause alarm. An expression devised for one context may not have meaning under another,
+ even if it evaluates without error. E.g., RIP will disappear when switching to a
+ 32-bit trace, or *:8 (*:8 (RSP+8)) may cause an invalid dereference if an x86
+ PUSH causes *:8 (RSP+8) to become 0.
+
+
+
Actions
+
+
The watches window provides the following actions:
+
+
Apply Data to Listing
+
+
This action is available when there's an active trace and at least one watch with an address
+ and data type is selected. If so, it applies that data type to the value in the listing. That
+ is, it attempts to apply the selected data type to the evaluated address, sizing it to the
+ value's size.
+
+
Select Range
+
+
This action is available when there's an active trace and at least one watch with memory
+ addresses is selected. It selects the memory range comprising the resulting value. This only
+ works when the outermost operator of the expression is a memory dereference. It selects the
+ range at the address of that dereference having the size of the dereference. For example, the
+ expression *:8 RSP would cause 8 bytes of memory, starting at the offset given by
+ RSP, to be selected in the dynamic listing.
+
+
Select Reads
+
+
This action is available when there's an active trace and at least one watch with memory
+ reads is selected. It selects all memory ranges dereferenced in the course of expression
+ evaluation. This can be useful when examining a watch whose value seems unusual. For example,
+ the expression *:8 RSP would cause 8 bytes of memory, starting at the offset given
+ by RSP, to be selected in the dynamic listing -- the same result as Select Range. However, the
+ expression *:4 (*:8 RSP) would cause two ranges to be selected: 8 bytes starting
+ at RSP and 4 bytes starting at the offset given by *:8 RSP.
+
+
Add
+
+
This action is always available. It adds a blank watch to the table.
+
+
Remove
+
+
This action is available when at least one watch is selected. It removes those watches.
+
+
Tool Options: Colors
+
+
The watch window uses colors to hint about changes in and freshness of displayed values.
+ They can be configured in the tool's options. By default, changed values are displayed in red,
+ and stale values are displayed in dark grey. A "stale" value is one which depends on any
+ register or memory whose contents are not known. The value displayed is that computed from the
+ last recorded contents, defaulting to 0 when never recorded. Simply, a "changed" watch is one
+ whose value has just changed. For example, if a value changes as result of stepping, then that
+ watch is changed. However, given the possibility of rewinding, changing thread focus, etc.,
+ "changed" is actually subtly more flexible. The watch remembers the evaluation from the user's
+ last coordinates (time, thread, frame, etc.) as well as the current coordinates. So, "changed"
+ more precisely refers to a watch whose value differs between those two coordinates. This
+ permits the user to switch focus between different coordinates and quickly identify what is
+ different.
+
+
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
new file mode 100644
index 0000000000..c9b63a6770
Binary files /dev/null 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/DebuggerCoordinates.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java
index d0bf327a4b..c9f042cc4f 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java
@@ -356,4 +356,8 @@ public class DebuggerCoordinates {
public boolean isPresent() {
return recorder.getSnap() == snap;
}
+
+ public boolean isAliveAndPresent() {
+ return isAlive() && isPresent();
+ }
}
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 9b80f11f6f..eeb6f969fc 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
@@ -121,7 +121,7 @@ public interface DebuggerResources {
// TODO: Draw an icon
ImageIcon ICON_SELECT_ADDRESSES = ResourceManager.loadImage("images/NextSelectionBlock16.gif");
// TODO: Draw an icon?
- ImageIcon ICON_CAPTURE_TYPES = ResourceManager.loadImage("images/dataTypes.png");
+ ImageIcon ICON_DATA_TYPES = ResourceManager.loadImage("images/dataTypes.png");
// TODO: Draw an icon?
ImageIcon ICON_CAPTURE_SYMBOLS = ResourceManager.loadImage("images/closedFolderLabels.png");
@@ -222,6 +222,15 @@ public interface DebuggerResources {
String OPTION_NAME_COLORS_REGISTER_CHANGED_SEL = "Colors.Changed Registers (selected)";
Color DEFAULT_COLOR_REGISTER_CHANGED_SEL = ColorUtils.blend(Color.RED, Color.WHITE, 0.5f);
+ String OPTION_NAME_COLORS_WATCH_STALE = "Colors.Stale Watches";
+ Color DEFAULT_COLOR_WATCH_STALE = Color.GRAY;
+ String OPTION_NAME_COLORS_WATCH_STALE_SEL = "Colors.Stale Watches (selected)";
+ Color DEFAULT_COLOR_WATCH_STALE_SEL = Color.LIGHT_GRAY;
+ String OPTION_NAME_COLORS_WATCH_CHANGED = "Colors.Changed Watches";
+ Color DEFAULT_COLOR_WATCH_CHANGED = Color.RED;
+ String OPTION_NAME_COLORS_WATCH_CHANGED_SEL = "Colors.Changed Watches (selected)";
+ Color DEFAULT_COLOR_WATCH_CHANGED_SEL = ColorUtils.blend(Color.RED, Color.WHITE, 0.5f);
+
String MARKER_NAME_BREAKPOINT_ENABLED = "Enabled Breakpoint";
String MARKER_NAME_BREAKPOINT_DISABLED = "Disabled Breakpoint";
String MARKER_NAME_BREAKPOINT_MIXED_ED = "Mixed Enabled-Disabled Breakpont";
@@ -1090,7 +1099,7 @@ public interface DebuggerResources {
abstract class AbstractCaptureTypesAction extends DockingAction {
public static final String NAME = "Capture Data Types";
- public static final Icon ICON = ICON_CAPTURE_TYPES;
+ public static final Icon ICON = ICON_DATA_TYPES;
public static final String HELP_ANCHOR = "capture_types";
public AbstractCaptureTypesAction(Plugin owner) {
@@ -1309,6 +1318,58 @@ public interface DebuggerResources {
}
}
+ interface ApplyDataTypeAction {
+ String NAME = "Apply Data to Listing ";
+ String DESCRIPTION =
+ "Apply the selected data type at the address of this value in the listing";
+ String GROUP = GROUP_GENERAL;
+ Icon ICON = ICON_DATA_TYPES;
+ String HELP_ANCHOR = "apply_data_type";
+
+ static ActionBuilder builder(Plugin owner) {
+ String ownerName = owner.getName();
+ return new ActionBuilder(NAME, ownerName)
+ .description(DESCRIPTION)
+ .toolBarGroup(GROUP)
+ .toolBarIcon(ICON)
+ .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
+ }
+ }
+
+ interface SelectWatchRangeAction {
+ String NAME = "Select Range";
+ String DESCRIPTION = "For memory watches, select the range comprising the value";
+ String GROUP = GROUP_GENERAL;
+ Icon ICON = ICON_SELECT_ADDRESSES;
+ String HELP_ANCHOR = "select_addresses";
+
+ static ActionBuilder builder(Plugin owner) {
+ String ownerName = owner.getName();
+ return new ActionBuilder(NAME, ownerName)
+ .description(DESCRIPTION)
+ .toolBarGroup(GROUP)
+ .toolBarIcon(ICON)
+ .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
+ }
+ }
+
+ interface SelectWatchReadsAction {
+ String NAME = "Select Reads";
+ String DESCRIPTION = "Select every memory range read evaluating this watch";
+ String GROUP = GROUP_GENERAL;
+ Icon ICON = ICON_REGIONS; // TODO: Meh. Better icon.
+ String HELP_ANCHOR = "select_reads";
+
+ static ActionBuilder builder(Plugin owner) {
+ String ownerName = owner.getName();
+ return new ActionBuilder(NAME, ownerName)
+ .description(DESCRIPTION)
+ .toolBarGroup(GROUP)
+ .toolBarIcon(ICON)
+ .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
+ }
+ }
+
public abstract class AbstractDebuggerConnectionsNode extends GTreeNode {
@Override
public String getName() {
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingAutoReadMemoryAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingAutoReadMemoryAction.java
index b9f7afc25b..80c861576b 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingAutoReadMemoryAction.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingAutoReadMemoryAction.java
@@ -104,10 +104,10 @@ public interface DebuggerListingAutoReadMemoryAction extends AutoReadMemoryActio
@Override
public CompletableFuture readMemory(DebuggerCoordinates coordinates,
AddressSetView visible) {
- TraceRecorder recorder = coordinates.getRecorder();
- if (recorder == null || !coordinates.isPresent()) {
+ if (!coordinates.isAliveAndPresent()) {
return AsyncUtils.NIL;
}
+ TraceRecorder recorder = coordinates.getRecorder();
AddressSet visibleAccessible =
recorder.getAccessibleProcessMemory().intersect(visible);
TraceMemoryManager mm = coordinates.getTrace().getMemoryManager();
@@ -135,10 +135,10 @@ public interface DebuggerListingAutoReadMemoryAction extends AutoReadMemoryActio
@Override
public CompletableFuture readMemory(DebuggerCoordinates coordinates,
AddressSetView visible) {
- TraceRecorder recorder = coordinates.getRecorder();
- if (recorder == null || !coordinates.isPresent()) {
+ if (!coordinates.isAliveAndPresent()) {
return AsyncUtils.NIL;
}
+ TraceRecorder recorder = coordinates.getRecorder();
AddressSet visibleAccessible =
recorder.getAccessibleProcessMemory().intersect(visible);
TraceMemoryManager mm = coordinates.getTrace().getMemoryManager();
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 9595314f2d..140f0a5ec8 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
@@ -301,7 +301,6 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter
}
class RegisterValueCellRenderer extends HexBigIntegerTableCellRenderer {
-
@Override
public final Component getTableCellRendererComponent(GTableCellRenderingData data) {
super.getTableCellRendererComponent(data);
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesPlugin.java
index 3f2f1c5322..2a3f0fa884 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesPlugin.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesPlugin.java
@@ -20,6 +20,7 @@ import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
import ghidra.app.services.*;
+import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
@@ -28,7 +29,7 @@ import ghidra.framework.plugintool.util.PluginStatus;
description = "GUI to watch values of expressions", //
category = PluginCategoryNames.DEBUGGER, //
packageName = DebuggerPluginPackage.NAME, //
- status = PluginStatus.UNSTABLE, //
+ status = PluginStatus.RELEASED, //
eventsConsumed = {
TraceActivatedPluginEvent.class, //
}, //
@@ -39,7 +40,6 @@ import ghidra.framework.plugintool.util.PluginStatus;
} //
)
public class DebuggerWatchesPlugin extends AbstractDebuggerPlugin {
- private static final String KEY_EXPRESSION_LIST = "expressionList";
private DebuggerWatchesProvider provider;
@@ -66,4 +66,14 @@ public class DebuggerWatchesPlugin extends AbstractDebuggerPlugin {
provider.coordinatesActivated(ev.getActiveCoordinates());
}
}
+
+ @Override
+ public void writeConfigState(SaveState saveState) {
+ provider.writeConfigState(saveState);
+ }
+
+ @Override
+ public void readConfigState(SaveState saveState) {
+ provider.readConfigState(saveState);
+ }
}
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 5ecce17f28..eab673f94c 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
@@ -15,61 +15,83 @@
*/
package ghidra.app.plugin.core.debug.gui.watch;
-import java.awt.BorderLayout;
+import java.awt.*;
import java.awt.event.MouseAdapter;
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 javax.swing.*;
+import javax.swing.table.TableColumn;
+import javax.swing.table.TableColumnModel;
+import docking.ActionContext;
import docking.WindowPosition;
-import docking.widgets.table.DefaultEnumeratedColumnTableModel;
+import docking.action.DockingAction;
+import docking.widgets.table.*;
import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
+import ghidra.app.plugin.core.debug.gui.DebuggerResources.*;
import ghidra.app.services.DebuggerListingService;
import ghidra.async.AsyncDebouncer;
import ghidra.async.AsyncTimer;
+import ghidra.base.widgets.table.DataTypeTableCellEditor;
+import ghidra.docking.settings.Settings;
import ghidra.framework.model.DomainObject;
import ghidra.framework.model.DomainObjectChangeRecord;
+import ghidra.framework.options.SaveState;
+import ghidra.framework.options.annotation.AutoOptionDefined;
+import ghidra.framework.options.annotation.HelpInfo;
import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
-import ghidra.lifecycle.Unfinished;
-import ghidra.program.model.address.Address;
-import ghidra.program.model.address.AddressSet;
+import ghidra.program.model.address.*;
import ghidra.program.model.data.DataType;
+import ghidra.program.model.data.DataTypeConflictException;
+import ghidra.program.model.listing.Data;
+import ghidra.program.model.listing.Listing;
+import ghidra.program.model.util.CodeUnitInsertionException;
+import ghidra.program.util.ProgramSelection;
import ghidra.trace.model.*;
import ghidra.trace.model.Trace.TraceMemoryBytesChangeType;
import ghidra.trace.model.Trace.TraceMemoryStateChangeType;
import ghidra.trace.util.TraceAddressSpace;
+import ghidra.util.Msg;
import ghidra.util.Swing;
+import ghidra.util.database.UndoableTransaction;
import ghidra.util.table.GhidraTable;
import ghidra.util.table.GhidraTableFilterPanel;
+import ghidra.util.table.column.AbstractGColumnRenderer;
+
+public class DebuggerWatchesProvider extends ComponentProviderAdapter {
+ private static final String KEY_EXPRESSION_LIST = "expressionList";
+ private static final String KEY_TYPE_LIST = "typeList";
-public class DebuggerWatchesProvider extends ComponentProviderAdapter implements Unfinished {
protected enum WatchTableColumns implements EnumeratedTableColumn {
EXPRESSION("Expression", String.class, WatchRow::getExpression, WatchRow::setExpression),
ADDRESS("Address", Address.class, WatchRow::getAddress),
- DATA_TYPE("Data Type", DataType.class, WatchRow::getDataType, WatchRow::setDataType),
- RAW("Raw", String.class, WatchRow::getRawValueString),
- VALUE("Value", String.class, WatchRow::getValueString),
- ERROR("Error", String.class, WatchRow::getError);
+ VALUE("Value", String.class, WatchRow::getRawValueString),
+ TYPE("Type", DataType.class, WatchRow::getDataType, WatchRow::setDataType),
+ REPR("Repr", String.class, WatchRow::getValueString),
+ ERROR("Error", String.class, WatchRow::getErrorMessage);
private final String header;
private final Function getter;
- private final BiConsumer setter;
+ private final BiConsumer setter;
private final Class> cls;
+ @SuppressWarnings("unchecked")
WatchTableColumns(String header, Class cls, Function getter,
BiConsumer setter) {
this.header = header;
this.cls = cls;
this.getter = getter;
- this.setter = setter;
+ this.setter = (BiConsumer) setter;
}
WatchTableColumns(String header, Class cls, Function getter) {
@@ -91,6 +113,11 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter implements
return header;
}
+ @Override
+ public void setValueOf(WatchRow row, Object value) {
+ setter.accept(row, value);
+ }
+
@Override
public boolean isEditable(WatchRow row) {
return setter != null;
@@ -137,49 +164,122 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter implements
}
private void bytesChanged(TraceAddressSpace space, TraceAddressSnapRange range) {
- if (space.getThread() == current.getThread()) {
+ if (space.getThread() == current.getThread() || space.getThread() == null) {
changed.add(range.getRange());
changeDebouncer.contact(null);
}
}
private void stateChanged(TraceAddressSpace space, TraceAddressSnapRange range) {
- if (space.getThread() == current.getThread()) {
+ if (space.getThread() == current.getThread() || space.getThread() == null) {
changed.add(range.getRange());
changeDebouncer.contact(null);
}
}
}
+ class WatchDataTypeEditor extends DataTypeTableCellEditor {
+ public WatchDataTypeEditor() {
+ super(plugin.getTool());
+ }
+
+ @Override
+ protected DataType resolveSelection(DataType dataType) {
+ if (dataType == null) {
+ return null;
+ }
+ try (UndoableTransaction tid =
+ UndoableTransaction.start(currentTrace, "Resolve DataType", true)) {
+ return currentTrace.getDataTypeManager().resolve(dataType, null);
+ }
+ }
+ }
+
+ class WatchValueCellRenderer extends AbstractGColumnRenderer {
+ @Override
+ public Component getTableCellRendererComponent(GTableCellRenderingData data) {
+ super.getTableCellRendererComponent(data);
+ WatchRow row = (WatchRow) data.getRowObject();
+ if (!row.isKnown()) {
+ if (data.isSelected()) {
+ setForeground(watchStaleSelColor);
+ }
+ else {
+ setForeground(watchStaleColor);
+ }
+ }
+ else if (row.isChanged()) {
+ if (data.isSelected()) {
+ setForeground(watchChangesSelColor);
+ }
+ else {
+ setForeground(watchChangesColor);
+ }
+ }
+ return this;
+ }
+
+ @Override
+ public String getFilterString(String t, Settings settings) {
+ return t;
+ }
+ }
+
+ final DebuggerWatchesPlugin plugin;
+
DebuggerCoordinates current = DebuggerCoordinates.NOWHERE;
private Trace currentTrace; // Copy for transition
@AutoServiceConsumed
- private DebuggerListingService listingService; // TODO: For goto
+ private DebuggerListingService listingService; // TODO: For goto and selection
// TODO: Allow address marking
@SuppressWarnings("unused")
private final AutoService.Wiring autoServiceWiring;
+ @AutoOptionDefined(name = DebuggerResources.OPTION_NAME_COLORS_WATCH_STALE, //
+ description = "Text color for watches whose value is not known", //
+ help = @HelpInfo(anchor = "colors"))
+ protected Color watchStaleColor = DebuggerResources.DEFAULT_COLOR_WATCH_STALE;
+ @AutoOptionDefined(name = DebuggerResources.OPTION_NAME_COLORS_WATCH_STALE_SEL, //
+ description = "Selected text color for watches whose value is not known", //
+ help = @HelpInfo(anchor = "colors"))
+ protected Color watchStaleSelColor = DebuggerResources.DEFAULT_COLOR_WATCH_STALE_SEL;
+ @AutoOptionDefined(name = DebuggerResources.OPTION_NAME_COLORS_WATCH_CHANGED, //
+ description = "Text color for watches whose value just changed", //
+ help = @HelpInfo(anchor = "colors"))
+ protected Color watchChangesColor = DebuggerResources.DEFAULT_COLOR_WATCH_CHANGED;
+ @AutoOptionDefined(name = DebuggerResources.OPTION_NAME_COLORS_WATCH_CHANGED_SEL, //
+ description = "Selected text color for watches whose value just changed", //
+ help = @HelpInfo(anchor = "colors"))
+ protected Color watchChangesSelColor = DebuggerResources.DEFAULT_COLOR_WATCH_CHANGED_SEL;
+
private final AddressSet changed = new AddressSet();
private final AsyncDebouncer changeDebouncer =
new AsyncDebouncer<>(AsyncTimer.DEFAULT_TIMER, 100);
private ForDepsListener forDepsListener = new ForDepsListener();
+ private JPanel mainPanel = new JPanel(new BorderLayout());
+
protected final WatchTableModel watchTableModel = new WatchTableModel();
protected GhidraTable watchTable;
protected GhidraTableFilterPanel watchFilterPanel;
- private JPanel mainPanel = new JPanel(new BorderLayout());
+ DockingAction actionApplyDataType;
+ DockingAction actionSelectRange;
+ DockingAction actionSelectAllReads;
+ DockingAction actionAdd;
+ DockingAction actionRemove;
private DebuggerWatchActionContext myActionContext;
public DebuggerWatchesProvider(DebuggerWatchesPlugin plugin) {
super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_WATCHES, plugin.getName());
+ this.plugin = plugin;
this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this);
setIcon(DebuggerResources.ICON_PROVIDER_WATCHES);
- setHelpLocation(DebuggerResources.HELP_PROVIDER_STACK);
+ setHelpLocation(DebuggerResources.HELP_PROVIDER_WATCHES);
setWindowMenuGroup(DebuggerPluginPackage.NAME);
buildMainPanel();
@@ -193,6 +293,14 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter implements
changeDebouncer.addListener(__ -> doCheckDepsAndReevaluate());
}
+ @Override
+ public ActionContext getActionContext(MouseEvent event) {
+ if (myActionContext != null) {
+ return myActionContext;
+ }
+ return super.getActionContext(event);
+ }
+
protected void buildMainPanel() {
watchTable = new GhidraTable(watchTableModel);
mainPanel.add(new JScrollPane(watchTable));
@@ -211,19 +319,34 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter implements
if (e.getClickCount() != 2 || e.getButton() != MouseEvent.BUTTON1) {
return;
}
- if (listingService == null) {
- return;
- }
if (myActionContext == null) {
return;
}
+ WatchRow row = myActionContext.getWatchRow();
+ if (row == null) {
+ return;
+ }
+ Throwable error = row.getError();
+ if (error != null) {
+ Msg.showError(this, getComponent(), "Evaluation error",
+ "Could not evaluate watch", error);
+ return;
+ }
Address address = myActionContext.getWatchRow().getAddress();
- if (address == null || !address.isMemoryAddress()) {
+ if (listingService == null || address == null || !address.isMemoryAddress()) {
return;
}
listingService.goTo(address, true);
}
});
+
+ TableColumnModel columnModel = watchTable.getColumnModel();
+ TableColumn addrCol = columnModel.getColumn(WatchTableColumns.ADDRESS.ordinal());
+ addrCol.setCellRenderer(CustomToStringCellRenderer.MONO_OBJECT);
+ TableColumn valCol = columnModel.getColumn(WatchTableColumns.VALUE.ordinal());
+ valCol.setCellRenderer(new WatchValueCellRenderer());
+ TableColumn typeCol = columnModel.getColumn(WatchTableColumns.TYPE.ordinal());
+ typeCol.setCellEditor(new WatchDataTypeEditor());
}
@Override
@@ -234,10 +357,162 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter implements
}
protected void createActions() {
- // TODO: Apply data type to listing
- // TODO: Select read addresses
- // TODO: Add
- // TODO: Remove
+ actionApplyDataType = ApplyDataTypeAction.builder(plugin)
+ .withContext(DebuggerWatchActionContext.class)
+ .enabledWhen(ctx -> current.getTrace() != null && selHasDataType(ctx))
+ .onAction(this::activatedApplyDataType)
+ .buildAndInstallLocal(this);
+ actionSelectRange = SelectWatchRangeAction.builder(plugin)
+ .withContext(DebuggerWatchActionContext.class)
+ .enabledWhen(ctx -> current.getTrace() != null && listingService != null &&
+ selHasMemoryRanges(ctx))
+ .onAction(this::activatedSelectRange)
+ .buildAndInstallLocal(this);
+ actionSelectAllReads = SelectWatchReadsAction.builder(plugin)
+ .withContext(DebuggerWatchActionContext.class)
+ .enabledWhen(ctx -> current.getTrace() != null && listingService != null &&
+ selHasMemoryReads(ctx))
+ .onAction(this::activatedSelectReads)
+ .buildAndInstallLocal(this);
+ actionAdd = AddAction.builder(plugin)
+ .onAction(this::activatedAdd)
+ .buildAndInstallLocal(this);
+ actionRemove = RemoveAction.builder(plugin)
+ .withContext(DebuggerWatchActionContext.class)
+ .enabledWhen(ctx -> !ctx.getWatchRows().isEmpty())
+ .onAction(this::activatedRemove)
+ .buildAndInstallLocal(this);
+ }
+
+ protected boolean selHasDataType(DebuggerWatchActionContext ctx) {
+ for (WatchRow row : ctx.getWatchRows()) {
+ Address address = row.getAddress();
+ if (row.getDataType() != null && address != null && address.isMemoryAddress() &&
+ row.getValueLength() != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected boolean selHasMemoryRanges(DebuggerWatchActionContext ctx) {
+ for (WatchRow row : ctx.getWatchRows()) {
+ AddressRange rng = row.getRange();
+ if (rng != null && rng.getAddressSpace().isMemorySpace()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected boolean selHasMemoryReads(DebuggerWatchActionContext ctx) {
+ for (WatchRow row : ctx.getWatchRows()) {
+ AddressSet set = row.getReads();
+ if (set == null) {
+ continue;
+ }
+ for (AddressRange rng : set) {
+ if (rng.getAddressSpace().isMemorySpace()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private void activatedApplyDataType(DebuggerWatchActionContext context) {
+ if (current.getTrace() == null) {
+ return;
+ }
+ List errs = new ArrayList<>();
+ for (WatchRow row : context.getWatchRows()) {
+ DataType dataType = row.getDataType();
+ if (dataType == null) {
+ continue;
+ }
+ Address address = row.getAddress();
+ if (address == null) {
+ continue;
+ }
+ if (!address.isMemoryAddress()) {
+ continue;
+ }
+ int size = row.getValueLength();
+ if (size == 0) {
+ continue;
+ }
+
+ // Using the view will handle the "from-now-until-whenever" logic.
+ Listing listing = current.getView().getListing();
+ // Avoid a transaction that just replaces it with an equivalent....
+ Data existing = listing.getDefinedDataAt(address);
+ if (existing != null) {
+ if (existing.getDataType().isEquivalent(dataType)) {
+ return;
+ }
+ }
+ try (UndoableTransaction tid =
+ UndoableTransaction.start(current.getTrace(), "Apply Watch Data Type", true)) {
+ try {
+ listing.clearCodeUnits(row.getAddress(), row.getRange().getMaxAddress(), false);
+ listing.createData(address, dataType, size);
+ }
+ catch (CodeUnitInsertionException | DataTypeConflictException e) {
+ errs.add(address + " " + dataType + "(" + size + "): " + e.getMessage());
+ }
+ }
+ }
+ if (!errs.isEmpty()) {
+ StringBuffer msg = new StringBuffer("One or more types could not be applied:");
+ for (String line : errs) {
+ msg.append("\n ");
+ msg.append(line);
+ }
+ Msg.showError(this, getComponent(), "Apply Data Type", msg.toString());
+ }
+ }
+
+ private void activatedSelectRange(DebuggerWatchActionContext context) {
+ if (listingService == null) {
+ return;
+ }
+ AddressSet sel = new AddressSet();
+ for (WatchRow row : context.getWatchRows()) {
+ AddressRange rng = row.getRange();
+ if (rng != null) {
+ sel.add(rng);
+ }
+ }
+ listingService.setCurrentSelection(new ProgramSelection(sel));
+ }
+
+ private void activatedSelectReads(DebuggerWatchActionContext context) {
+ if (listingService == null) {
+ return;
+ }
+ AddressSet sel = new AddressSet();
+ for (WatchRow row : context.getWatchRows()) {
+ AddressSet reads = row.getReads();
+ if (reads != null) {
+ sel.add(reads);
+ }
+ }
+ listingService.setCurrentSelection(new ProgramSelection(sel));
+ }
+
+ private void activatedAdd(ActionContext ignored) {
+ addWatch("");
+ }
+
+ private void activatedRemove(DebuggerWatchActionContext context) {
+ watchTableModel.deleteWith(context.getWatchRows()::contains);
+ }
+
+ public WatchRow addWatch(String expression) {
+ WatchRow row = new WatchRow(this, expression);
+ row.setCoordinates(current);
+ watchTableModel.add(row);
+ return row;
}
@Override
@@ -277,28 +552,39 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter implements
doSetTrace(current.getTrace());
- if (current.getRecorder() != null) {
+ setRowsContext(coordinates);
+
+ if (current.isAliveAndPresent()) {
readTarget();
}
reevaluate();
+ Swing.runIfSwingOrRunLater(() -> watchTableModel.fireTableDataChanged());
+ }
+
+ public synchronized void setRowsContext(DebuggerCoordinates coordinates) {
+ for (WatchRow row : watchTableModel.getModelData()) {
+ row.setCoordinates(coordinates);
+ }
}
public synchronized void readTarget() {
for (WatchRow row : watchTableModel.getModelData()) {
- if (row.getReads().intersects(changed)) {
- row.doTargetReads();
- }
+ row.doTargetReads();
}
}
public synchronized void doCheckDepsAndReevaluate() {
for (WatchRow row : watchTableModel.getModelData()) {
- if (row.getReads().intersects(changed)) {
+ AddressSet reads = row.getReads();
+ if (reads == null || reads.intersects(changed)) {
row.reevaluate();
}
}
changed.clear();
- Swing.runIfSwingOrRunLater(() -> watchTableModel.fireTableDataChanged());
+ Swing.runIfSwingOrRunLater(() -> {
+ watchTableModel.fireTableDataChanged();
+ contextChanged();
+ });
}
public void reevaluate() {
@@ -307,4 +593,29 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter implements
}
changed.clear();
}
+
+ public void writeConfigState(SaveState saveState) {
+ List rows = List.copyOf(watchTableModel.getModelData());
+ String[] expressions = rows.stream().map(WatchRow::getExpression).toArray(String[]::new);
+ String[] types = rows.stream().map(WatchRow::getTypePath).toArray(String[]::new);
+ saveState.putStrings(KEY_EXPRESSION_LIST, expressions);
+ saveState.putStrings(KEY_TYPE_LIST, types);
+ }
+
+ public void readConfigState(SaveState saveState) {
+ String[] expressions = saveState.getStrings(KEY_EXPRESSION_LIST, new String[] {});
+ String[] types = saveState.getStrings(KEY_TYPE_LIST, new String[] {});
+ if (expressions.length != types.length) {
+ Msg.error(this, "Watch provider config error. Unequal number of expressions and types");
+ return;
+ }
+ int len = expressions.length;
+ List rows = new ArrayList<>();
+ for (int i = 0; i < len; i++) {
+ WatchRow r = new WatchRow(this, expressions[i]);
+ r.setTypePath(types[i]);
+ rows.add(r);
+ }
+ watchTableModel.addAll(rows);
+ }
}
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 eb97e81931..d1a14e0647 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
@@ -15,14 +15,20 @@
*/
package ghidra.app.plugin.core.debug.gui.watch;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Objects;
+
import org.apache.commons.lang3.tuple.Pair;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.app.plugin.processors.sleigh.SleighLanguage;
+import ghidra.app.services.DataTypeManagerService;
import ghidra.docking.settings.SettingsImpl;
import ghidra.pcode.exec.*;
import ghidra.pcode.exec.trace.TraceBytesPcodeExecutorState;
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.lang.Language;
@@ -37,12 +43,14 @@ import ghidra.util.Swing;
public class WatchRow {
private final DebuggerWatchesProvider provider;
+ private Trace trace;
private SleighLanguage language;
private PcodeExecutor> executorWithState;
private ReadDepsPcodeExecutor executorWithAddress;
private AsyncPcodeExecutor asyncExecutor;
private String expression;
+ private String typePath;
private DataType dataType;
private SleighExpression compiled;
@@ -50,8 +58,9 @@ public class WatchRow {
private Address address;
private AddressSet reads;
private byte[] value;
+ private byte[] prevValue; // Value at previous coordinates
private String valueString;
- private String error = "";
+ private Throwable error = null;
public WatchRow(DebuggerWatchesProvider provider, String expression) {
this.provider = provider;
@@ -59,8 +68,6 @@ public class WatchRow {
}
protected void blank() {
- error = null;
- compiled = null;
state = null;
address = null;
reads = null;
@@ -69,20 +76,27 @@ public class WatchRow {
}
protected void recompile() {
- this.error = null;
+ compiled = null;
+ error = null;
+ if (expression == null || expression.length() == 0) {
+ return;
+ }
+ if (language == null) {
+ return;
+ }
try {
- this.compiled = SleighProgramCompiler.compileExpression(language, expression);
+ compiled = SleighProgramCompiler.compileExpression(language, expression);
}
catch (Exception e) {
- this.error = e.getMessage();
+ error = e;
return;
}
}
protected void doTargetReads() {
- if (asyncExecutor != null) {
+ if (compiled != null && asyncExecutor != null) {
compiled.evaluate(asyncExecutor).exceptionally(ex -> {
- error = ex.getMessage();
+ error = ex;
Swing.runIfSwingOrRunLater(() -> {
provider.watchTableModel.notifyUpdated(this);
});
@@ -94,11 +108,15 @@ public class WatchRow {
protected void reevaluate() {
blank();
+ if (trace == null || compiled == null) {
+ return;
+ }
try {
Pair valueWithState = compiled.evaluate(executorWithState);
Pair valueWithAddress = compiled.evaluate(executorWithAddress);
value = valueWithState.getLeft();
+ error = null;
state = valueWithState.getRight();
address = valueWithAddress.getRight();
reads = executorWithAddress.getReads();
@@ -106,12 +124,12 @@ public class WatchRow {
valueString = parseAsDataType();
}
catch (Exception e) {
- error = e.getMessage();
+ error = e;
}
}
protected String parseAsDataType() {
- if (dataType == null) {
+ if (dataType == null || value == null) {
return "";
}
MemBuffer buffer = new ByteMemBufferImpl(address, value, language.isBigEndian());
@@ -128,14 +146,19 @@ public class WatchRow {
}
@Override
- protected byte[] getFromSpace(TraceMemorySpace space, long offset, int size) {
- byte[] data = super.getFromSpace(space, offset, size);
- try {
- reads.add(
- new AddressRangeImpl(space.getAddressSpace().getAddress(offset), data.length));
+ public byte[] getVar(AddressSpace space, long offset, int size,
+ boolean truncateAddressableUnit) {
+ byte[] data = super.getVar(space, offset, size, truncateAddressableUnit);
+ if (space.isMemorySpace()) {
+ offset = truncateOffset(space, offset);
}
- catch (AddressOverflowException | AddressOutOfBoundsException e) {
- throw new AssertionError(e);
+ if (space.isMemorySpace() || space.isRegisterSpace()) {
+ try {
+ reads.add(new AddressRangeImpl(space.getAddress(offset), data.length));
+ }
+ catch (AddressOverflowException | AddressOutOfBoundsException e) {
+ throw new AssertionError(e);
+ }
}
return data;
}
@@ -191,46 +214,85 @@ public class WatchRow {
return new ReadDepsPcodeExecutor(state, language, arithmetic, paired);
}
- public void setContext(DebuggerCoordinates coordinates) {
- Trace trace = coordinates.getTrace();
+ public void setCoordinates(DebuggerCoordinates coordinates) {
+ // NB. Caller has already verified coordinates actually changed
+ prevValue = value;
+ trace = coordinates.getTrace();
+ updateType();
if (trace == null) {
blank();
- error = "No trace nor thread active";
return;
}
Language newLanguage = trace.getBaseLanguage();
- if (this.language != newLanguage) {
+ if (language != newLanguage) {
if (!(newLanguage instanceof SleighLanguage)) {
- error = "No a sleigh-based langauge";
+ error = new RuntimeException("Not a sleigh-based langauge");
return;
}
- this.language = (SleighLanguage) newLanguage;
+ language = (SleighLanguage) newLanguage;
recompile();
}
- boolean live = coordinates.isAlive() && coordinates.isPresent();
- if (live) {
- this.asyncExecutor = TracePcodeUtils.executorForCoordinates(coordinates);
+ if (coordinates.isAliveAndPresent()) {
+ asyncExecutor = TracePcodeUtils.executorForCoordinates(coordinates);
}
- this.executorWithState = TraceSleighUtils.buildByteWithStateExecutor(trace,
+ executorWithState = TraceSleighUtils.buildByteWithStateExecutor(trace,
coordinates.getSnap(), coordinates.getThread(), coordinates.getFrame());
- this.executorWithAddress = buildAddressDepsExecutor(coordinates);
- if (live) {
- doTargetReads();
- }
- reevaluate(); // NB. Target reads may not cause database changes
+ executorWithAddress = buildAddressDepsExecutor(coordinates);
}
public void setExpression(String expression) {
+ if (!Objects.equals(this.expression, expression)) {
+ prevValue = null;
+ // NB. Allow fall-through so user can re-evaluate via nop edit.
+ }
this.expression = expression;
+ blank();
recompile();
+ if (error != null) {
+ provider.contextChanged();
+ return;
+ }
+ if (asyncExecutor != null) {
+ doTargetReads();
+ }
+ reevaluate();
+ provider.contextChanged();
}
public String getExpression() {
return expression;
}
+ protected void updateType() {
+ dataType = null;
+ if (trace == null || typePath == null) {
+ return;
+ }
+ dataType = trace.getDataTypeManager().getDataType(typePath);
+ if (dataType != null) {
+ return;
+ }
+ DataTypeManagerService dtms = provider.getTool().getService(DataTypeManagerService.class);
+ if (dtms == null) {
+ return;
+ }
+ dataType = dtms.getBuiltInDataTypesManager().getDataType(typePath);
+ }
+
+ public void setTypePath(String typePath) {
+ this.typePath = typePath;
+ updateType();
+ }
+
+ public String getTypePath() {
+ return typePath;
+ }
+
public void setDataType(DataType dataType) {
+ this.typePath = dataType == null ? null : dataType.getPathName();
this.dataType = dataType;
+ valueString = parseAsDataType();
+ provider.contextChanged();
}
public DataType getDataType() {
@@ -241,11 +303,34 @@ public class WatchRow {
return address;
}
- public String getRawValueString() {
- if (value.length > 20) {
- return NumericUtilities.convertBytesToString(value, 0, 20, " ") + "...";
+ public AddressRange getRange() {
+ if (address == null || value == null) {
+ return null;
}
- return NumericUtilities.convertBytesToString(value, " ");
+ if (address.isConstantAddress()) {
+ return new AddressRangeImpl(address, address);
+ }
+ try {
+ return new AddressRangeImpl(address, value.length);
+ }
+ catch (AddressOverflowException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ public String getRawValueString() {
+ if (value == null) {
+ return "??";
+ }
+ if (address == null || !address.getAddressSpace().isMemorySpace()) {
+ BigInteger asBigInt =
+ Utils.bytesToBigInteger(value, value.length, language.isBigEndian(), false);
+ return "0x" + asBigInt.toString(16);
+ }
+ if (value.length > 20) {
+ return "{ " + NumericUtilities.convertBytesToString(value, 0, 20, " ") + " ... }";
+ }
+ return "{ " + NumericUtilities.convertBytesToString(value, " ") + " }";
}
public AddressSet getReads() {
@@ -260,7 +345,33 @@ public class WatchRow {
return valueString;
}
- public String getError() {
+ public int getValueLength() {
+ return value == null ? 0 : value.length;
+ }
+
+ public String getErrorMessage() {
+ if (error == null) {
+ return "";
+ }
+ String message = error.getMessage();
+ if (message != null && message.trim().length() != 0) {
+ return message;
+ }
+ return error.getClass().getSimpleName();
+ }
+
+ public Throwable getError() {
return error;
}
+
+ public boolean isKnown() {
+ return state == TraceMemoryState.KNOWN;
+ }
+
+ public boolean isChanged() {
+ if (prevValue == null) {
+ return false;
+ }
+ return !Arrays.equals(value, prevValue);
+ }
}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java
index 44783bc203..303ba304fa 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java
@@ -890,13 +890,10 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
protected static TargetObjectRef translateToFocus(DebuggerCoordinates prev,
DebuggerCoordinates resolved) {
+ if (!resolved.isAliveAndPresent()) {
+ return null;
+ }
TraceRecorder recorder = resolved.getRecorder();
- if (recorder == null) {
- return null;
- }
- if (!resolved.isPresent()) {
- return null;
- }
if (!Objects.equals(prev.getFrame(), resolved.getFrame())) {
TargetStackFrame> frame =
recorder.getTargetStackFrame(resolved.getThread(), resolved.getFrame());
diff --git a/Ghidra/Debug/Debugger/src/main/resources/defaultTools/Debugger.tool b/Ghidra/Debug/Debugger/src/main/resources/defaultTools/Debugger.tool
index 38df329c0d..d24e0a8845 100644
--- a/Ghidra/Debug/Debugger/src/main/resources/defaultTools/Debugger.tool
+++ b/Ghidra/Debug/Debugger/src/main/resources/defaultTools/Debugger.tool
@@ -1,157 +1,151 @@
-
-
+
+
+
+
-
-
-
-
-
+
-
+
-
+
-
-
-
+
+
+
-
-
-
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
-
-
+
+
+
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
-
-
+
-
+
@@ -159,9 +153,9 @@
-
+
-
+
@@ -199,6 +193,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -306,10 +314,10 @@
-
-
+
+
-
+
@@ -373,6 +381,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -413,6 +439,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -428,13 +467,13 @@
-
+
-
-
+
+
-
+
@@ -457,6 +496,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -525,6 +579,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -556,6 +626,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesPluginScreenShots.java
new file mode 100644
index 0000000000..0f6855f156
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesPluginScreenShots.java
@@ -0,0 +1,91 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.gui.watch;
+
+import org.junit.*;
+
+import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin;
+import ghidra.app.services.DebuggerTraceManagerService;
+import ghidra.pcode.exec.PcodeExecutor;
+import ghidra.pcode.exec.trace.TraceSleighUtils;
+import ghidra.program.model.data.FloatDataType;
+import ghidra.program.model.data.LongDataType;
+import ghidra.test.ToyProgramBuilder;
+import ghidra.trace.database.ToyDBTraceBuilder;
+import ghidra.trace.model.thread.TraceThread;
+import ghidra.util.database.UndoableTransaction;
+import help.screenshot.GhidraScreenShotGenerator;
+
+public class DebuggerWatchesPluginScreenShots extends GhidraScreenShotGenerator {
+
+ DebuggerTraceManagerService traceManager;
+ DebuggerWatchesPlugin watchesPlugin;
+ DebuggerWatchesProvider watchesProvider;
+ ToyDBTraceBuilder tb;
+
+ @Before
+ public void setUpMin() throws Throwable {
+ traceManager = addPlugin(tool, DebuggerTraceManagerServicePlugin.class);
+ watchesPlugin = addPlugin(tool, DebuggerWatchesPlugin.class);
+
+ watchesProvider = waitForComponentProvider(DebuggerWatchesProvider.class);
+
+ tb = new ToyDBTraceBuilder("echo", ToyProgramBuilder._X64);
+ }
+
+ @After
+ public void tearDownMine() {
+ tb.close();
+ }
+
+ @Test
+ public void testCaptureDebuggerWatchesPlugin() throws Throwable {
+ TraceThread thread;
+ long snap0, snap1;
+ try (UndoableTransaction tid = tb.startTransaction()) {
+ snap0 = tb.trace.getTimeManager().createSnapshot("First").getKey();
+ snap1 = tb.trace.getTimeManager().createSnapshot("Second").getKey();
+
+ thread = tb.getOrAddThread("[1]", snap0);
+
+ PcodeExecutor executor0 =
+ TraceSleighUtils.buildByteExecutor(tb.trace, snap0, thread, 0);
+ executor0.executeLine("RSP = 0x7ffefff8");
+ executor0.executeLine("*:4 (RSP+8) = 0x4030201");
+
+ PcodeExecutor executor1 =
+ TraceSleighUtils.buildByteExecutor(tb.trace, snap1, thread, 0);
+ executor1.executeLine("RSP = 0x7ffefff8");
+ executor1.executeLine("*:4 (RSP+8) = 0x1020304");
+ executor1.executeLine("*:4 0x7fff0004:8 = 0x4A9A70C8");
+ }
+
+ watchesProvider.addWatch("RSP");
+ watchesProvider.addWatch("*:8 RSP");
+ watchesProvider.addWatch("*:4 (RSP+8)").setDataType(LongDataType.dataType);
+ watchesProvider.addWatch("*:4 0x7fff0004:8").setDataType(FloatDataType.dataType);
+
+ traceManager.openTrace(tb.trace);
+ traceManager.activateThread(thread);
+ waitForSwing();
+ traceManager.activateSnap(snap0);
+ waitForSwing();
+ traceManager.activateSnap(snap1);
+ waitForSwing();
+
+ captureIsolatedProvider(watchesProvider, 700, 400);
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java
index 068cb999a9..05845f519e 100644
--- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java
+++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java
@@ -991,7 +991,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI
// First check nothing captured yet
buf.clear();
- trace.getMemoryManager().getBytes(recorder.getSnap(), addr(trace, 0x55550000), buf);
+ assertEquals(data.length,
+ trace.getMemoryManager().getBytes(recorder.getSnap(), addr(trace, 0x55550000), buf));
assertArrayEquals(zero, buf.array());
// Verify that the action performs the expected task
@@ -1001,7 +1002,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI
waitForPass(() -> {
buf.clear();
- trace.getMemoryManager().getBytes(recorder.getSnap(), addr(trace, 0x55550000), buf);
+ assertEquals(data.length, trace.getMemoryManager()
+ .getBytes(recorder.getSnap(), addr(trace, 0x55550000), buf));
// NOTE: The region is only 256 bytes long
// TODO: This fails unpredictably, and I'm not sure why.
assertArrayEquals(Arrays.copyOf(data, 256), Arrays.copyOf(buf.array(), 256));
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
new file mode 100644
index 0000000000..d3575a6e1a
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/watch/DebuggerWatchesProviderTest.java
@@ -0,0 +1,234 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.gui.watch;
+
+import static org.junit.Assert.*;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+
+import org.junit.*;
+
+import generic.Unique;
+import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
+import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin;
+import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceTest;
+import ghidra.app.services.TraceRecorder;
+import ghidra.dbg.model.TestTargetRegisterBankInThread;
+import ghidra.program.model.address.Address;
+import ghidra.program.model.address.AddressRangeImpl;
+import ghidra.program.model.data.LongDataType;
+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.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 {
+ static {
+ DebuggerModelServiceTest.addTestModelPathPatterns();
+ }
+
+ protected static void assertNoErr(WatchRow row) {
+ Throwable error = row.getError();
+ if (error != null) {
+ throw new AssertionError(error);
+ }
+ }
+
+ protected DebuggerWatchesPlugin watchesPlugin;
+ protected DebuggerWatchesProvider watchesProvider;
+ protected DebuggerListingPlugin listingPlugin;
+
+ protected Register r0;
+ protected TraceThread thread;
+
+ @Before
+ public void setUpWatchesProviderTest() throws Exception {
+ watchesPlugin = addPlugin(tool, DebuggerWatchesPlugin.class);
+ watchesProvider = waitForComponentProvider(DebuggerWatchesProvider.class);
+ listingPlugin = addPlugin(tool, DebuggerListingPlugin.class);
+
+ createTrace();
+ r0 = tb.language.getRegister("r0");
+ try (UndoableTransaction tid = tb.startTransaction()) {
+ thread = tb.getOrAddThread("Thread1", 0);
+ }
+ }
+
+ @After
+ public void tearDownWatchesProviderTest() throws Exception {
+ for (WatchRow row : watchesProvider.watchTableModel.getModelData()) {
+ Throwable error = row.getError();
+ if (error != null) {
+ Msg.info(this, "Error on watch row: ", error);
+ }
+ }
+ }
+
+ private void setRegisterValues(TraceThread thread) {
+ try (UndoableTransaction tid = tb.startTransaction()) {
+ TraceMemoryRegisterSpace regVals =
+ tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, true);
+ regVals.setValue(0, new RegisterValue(r0, BigInteger.valueOf(0x00400000)));
+ }
+ }
+
+ @Test
+ public void testAddValsAddWatchThenActivateThread() {
+ setRegisterValues(thread);
+
+ performAction(watchesProvider.actionAdd);
+ WatchRow row = Unique.assertOne(watchesProvider.watchTableModel.getModelData());
+ row.setExpression("r0");
+
+ traceManager.openTrace(tb.trace);
+ traceManager.activateThread(thread);
+ waitForSwing();
+
+ assertEquals("0x400000", row.getRawValueString());
+ assertEquals("", row.getValueString()); // NB. No data type set
+ assertNoErr(row);
+ }
+
+ @Test
+ public void testActivateThreadAddWatchThenAddVals() {
+ traceManager.openTrace(tb.trace);
+ traceManager.activateThread(thread);
+ waitForSwing();
+
+ performAction(watchesProvider.actionAdd);
+ WatchRow row = Unique.assertOne(watchesProvider.watchTableModel.getModelData());
+ row.setExpression("r0");
+
+ setRegisterValues(thread);
+
+ waitForPass(() -> assertEquals("0x400000", row.getRawValueString()));
+ assertNoErr(row);
+ }
+
+ @Test
+ public void testWatchWithDataType() {
+ setRegisterValues(thread);
+
+ performAction(watchesProvider.actionAdd);
+ WatchRow row = Unique.assertOne(watchesProvider.watchTableModel.getModelData());
+ row.setExpression("r0");
+ row.setDataType(LongLongDataType.dataType);
+
+ traceManager.openTrace(tb.trace);
+ traceManager.activateThread(thread);
+ waitForSwing();
+
+ assertEquals("0x400000", row.getRawValueString());
+ assertEquals("400000h", row.getValueString());
+ assertNoErr(row);
+
+ assertEquals(r0.getAddress(), row.getAddress());
+ assertEquals(TraceRegisterUtils.rangeForRegister(r0), row.getRange());
+ }
+
+ @Test
+ public void testConstantWatch() {
+ setRegisterValues(thread);
+
+ performAction(watchesProvider.actionAdd);
+ WatchRow row = Unique.assertOne(watchesProvider.watchTableModel.getModelData());
+ row.setExpression("0xdeadbeef:4");
+ row.setDataType(LongDataType.dataType);
+
+ traceManager.openTrace(tb.trace);
+ traceManager.activateThread(thread);
+ waitForSwing();
+
+ assertEquals("{ de ad be ef }", row.getRawValueString());
+ assertEquals("DEADBEEFh", row.getValueString());
+ assertNoErr(row);
+
+ Address constDeadbeef = tb.trace.getBaseAddressFactory().getConstantAddress(0xdeadbeefL);
+ assertEquals(constDeadbeef, row.getAddress());
+ assertEquals(new AddressRangeImpl(constDeadbeef, constDeadbeef), row.getRange());
+ }
+
+ @Test
+ public void testUniqueWatch() {
+ setRegisterValues(thread);
+
+ performAction(watchesProvider.actionAdd);
+ WatchRow row = Unique.assertOne(watchesProvider.watchTableModel.getModelData());
+ row.setExpression("r0 + 8");
+ row.setDataType(LongLongDataType.dataType);
+
+ traceManager.openTrace(tb.trace);
+ traceManager.activateThread(thread);
+ waitForSwing();
+
+ assertEquals("{ 00 00 00 00 00 40 00 08 }", row.getRawValueString());
+ assertEquals("400008h", row.getValueString());
+ assertNoErr(row);
+
+ assertNull(row.getAddress());
+ assertNull(row.getRange());
+ }
+
+ @Test
+ public void testLiveCausesReads() throws Exception {
+ createTestModel();
+ mb.createTestProcessesAndThreads();
+ TestTargetRegisterBankInThread 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);
+ bank.writeRegister("r0", tb.arr(0, 0, 0, 0, 0, 0x40, 0, 0));
+ mb.testProcess1.addRegion(".header", mb.rng(0, 0x1000), "r"); // Keep the listing away
+ 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,
+ new TestDebuggerTargetTraceMapper(mb.testProcess1));
+ Trace trace = recorder.getTrace();
+ TraceThread thread = waitForValue(() -> recorder.getTraceThread(mb.testThread1));
+
+ traceManager.openTrace(trace);
+ traceManager.activateThread(thread);
+ waitForSwing();
+
+ // Verify no target read has occurred yet
+ TraceMemoryRegisterSpace regs =
+ trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
+ if (regs != null) {
+ assertEquals(BigInteger.ZERO, regs.getValue(0, r0).getUnsignedValue());
+ }
+ ByteBuffer buf = ByteBuffer.allocate(4);
+ assertEquals(4, trace.getMemoryManager().getBytes(0, tb.addr(0x00400000), buf));
+ assertArrayEquals(tb.arr(0, 0, 0, 0), buf.array());
+
+ performAction(watchesProvider.actionAdd);
+ WatchRow row = Unique.assertOne(watchesProvider.watchTableModel.getModelData());
+ row.setExpression("*:4 r0");
+ row.setDataType(LongDataType.dataType);
+
+ waitForPass(() -> {
+ assertEquals("{ 01 02 03 04 }", row.getRawValueString());
+ assertEquals("1020304h", row.getValueString());
+ });
+ assertNoErr(row);
+ }
+}
diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java
index a43427b96c..8bd2897c82 100644
--- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java
+++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java
@@ -41,6 +41,7 @@ import ghidra.trace.model.TraceAddressSnapRange;
import ghidra.trace.model.memory.*;
import ghidra.trace.model.stack.TraceStackFrame;
import ghidra.trace.model.thread.TraceThread;
+import ghidra.util.MathUtilities;
import ghidra.util.UnionAddressSetView;
import ghidra.util.database.DBOpenMode;
import ghidra.util.exception.DuplicateNameException;
@@ -258,7 +259,12 @@ public class DBTraceMemoryManager
@Override
public int getBytes(long snap, Address start, ByteBuffer buf) {
- return delegateReadI(start.getAddressSpace(), m -> m.getBytes(snap, start, buf), 0);
+ return delegateReadI(start.getAddressSpace(), m -> m.getBytes(snap, start, buf), () -> {
+ Address max = start.getAddressSpace().getMaxAddress();
+ int len = MathUtilities.unsignedMin(buf.remaining(), max.subtract(start));
+ buf.position(buf.position() + len);
+ return len;
+ });
}
@Override
diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/space/DBTraceDelegatingManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/space/DBTraceDelegatingManager.java
index b7b2435723..2ec35e7d77 100644
--- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/space/DBTraceDelegatingManager.java
+++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/space/DBTraceDelegatingManager.java
@@ -105,6 +105,17 @@ public interface DBTraceDelegatingManager {
}
}
+ default int delegateReadI(AddressSpace space, ToIntFunction func, IntSupplier ifNull) {
+ checkIsInMemory(space);
+ try (LockHold hold = LockHold.lock(readLock())) {
+ M m = getForSpace(space, false);
+ if (m == null) {
+ return ifNull.getAsInt();
+ }
+ return func.applyAsInt(m);
+ }
+ }
+
default boolean delegateReadB(AddressSpace space, Predicate func, boolean ifNull) {
checkIsInMemory(space);
try (LockHold hold = LockHold.lock(readLock())) {
diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/memory/AbstractDBTraceMemoryManagerTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/memory/AbstractDBTraceMemoryManagerTest.java
index c6c33bf550..ff2171e27c 100644
--- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/memory/AbstractDBTraceMemoryManagerTest.java
+++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/memory/AbstractDBTraceMemoryManagerTest.java
@@ -831,8 +831,7 @@ public abstract class AbstractDBTraceMemoryManagerTest
assertEquals(expected, collectAsMap(memory.getStates(3, range(0x3000, 0x5000))));
ByteBuffer read = ByteBuffer.allocate(4);
- // NOTE: 0 is returned because the space is no longer active....
- assertEquals(0, memory.getBytes(3, addr(0x4000), read));
+ assertEquals(4, memory.getBytes(3, addr(0x4000), read));
assertArrayEquals(arr(0, 0, 0, 0), read.array());
}
@@ -860,8 +859,7 @@ public abstract class AbstractDBTraceMemoryManagerTest
assertEquals(expected, collectAsMap(memory.getStates(3, range(0x3000, 0x5000))));
ByteBuffer read = ByteBuffer.allocate(4);
- // NOTE: 0 is returned because the space is no longer active....
- assertEquals(0, memory.getBytes(3, addr(0x4000), read));
+ assertEquals(4, memory.getBytes(3, addr(0x4000), read));
assertArrayEquals(arr(0, 0, 0, 0), read.array());
}
@@ -889,8 +887,7 @@ public abstract class AbstractDBTraceMemoryManagerTest
assertEquals(expected, collectAsMap(memory.getStates(3, range(0x3000, 0x5000))));
ByteBuffer read = ByteBuffer.allocate(4);
- // NOTE: 0 is returned because the space is no longer active....
- assertEquals(0, memory.getBytes(3, addr(0x4000), read));
+ assertEquals(4, memory.getBytes(3, addr(0x4000), read));
assertArrayEquals(arr(0, 0, 0, 0), read.array());
trace.redo();
diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorState.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorState.java
index 7928d89596..b373a2f137 100644
--- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorState.java
+++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorState.java
@@ -39,7 +39,7 @@ public class AddressOfPcodeExecutorState
@Override
public void setVar(AddressSpace space, byte[] offset, int size,
boolean truncateAddressableUnit, Address val) {
- if (space != unique) {
+ if (!space.isUniqueSpace()) {
return;
}
long off = Utils.bytesToLong(offset, offset.length, isBigEndian);
@@ -50,7 +50,7 @@ public class AddressOfPcodeExecutorState
public Address getVar(AddressSpace space, byte[] offset, int size,
boolean truncateAddressableUnit) {
long off = Utils.bytesToLong(offset, offset.length, isBigEndian);
- if (space != unique) {
+ if (!space.isUniqueSpace()) {
return space.getAddress(off);
}
return unique.get(off);
diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java
index fe116bbb22..8174c4a810 100644
--- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java
+++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutor.java
@@ -18,6 +18,7 @@ package ghidra.pcode.exec;
import java.util.List;
import java.util.Map;
+import ghidra.app.plugin.processors.sleigh.SleighLanguage;
import ghidra.pcode.error.LowlevelError;
import ghidra.pcode.exec.SleighUseropLibrary.SleighUseropDefinition;
import ghidra.pcode.opbehavior.*;
@@ -45,6 +46,12 @@ public class PcodeExecutor {
this.pointerSize = language.getDefaultSpace().getPointerSize();
}
+ public void executeLine(String line) {
+ SleighProgram program = SleighProgramCompiler.compileProgram((SleighLanguage) language,
+ "line", List.of(line + ";"), SleighUseropLibrary.NIL);
+ execute(program, SleighUseropLibrary.nil());
+ }
+
public void execute(SleighProgram program, SleighUseropLibrary library) {
execute(program.code, program.useropNames, library);
}