diff --git a/Ghidra/Debug/Debugger/certification.manifest b/Ghidra/Debug/Debugger/certification.manifest index 241b0602fb..21cfd14c2d 100644 --- a/Ghidra/Debug/Debugger/certification.manifest +++ b/Ghidra/Debug/Debugger/certification.manifest @@ -101,6 +101,8 @@ src/main/help/help/topics/DebuggerThreadsPlugin/images/stepinto.png||GHIDRA||||E src/main/help/help/topics/DebuggerTimePlugin/DebuggerTimePlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerTimePlugin/images/DebuggerTimePlugin.png||GHIDRA||||END| src/main/help/help/topics/DebuggerTraceManagerServicePlugin/DebuggerTraceManagerServicePlugin.html||GHIDRA||||END| +src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html||GHIDRA||||END| +src/main/help/help/topics/DebuggerWatchesPlugin/images/DebuggerWatchesPlugin.png||GHIDRA||||END| src/main/resources/defaultTools/Debugger.tool||GHIDRA||||END| src/main/resources/define_info_proc_mappings||GHIDRA||||END| src/main/resources/images/add.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| diff --git a/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml b/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml index 6e53ece5b8..52045e9a48 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml @@ -128,6 +128,10 @@ target="help/topics/DebuggerStaticMappingPlugin/DebuggerStaticMappingPlugin.html" /> + + diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html new file mode 100644 index 0000000000..1c2a00fc3a --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html @@ -0,0 +1,135 @@ + + + + + + + Debugger: Watches + + + + + +

Debugger: Watches

+ + + + + + + +
+ +

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:

+ + + +

Table Columns

+ +

The table displays and allows modification of each watch. It has the following columns:

+ + + +

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); }