The Variable Viewer displays a table of local variables for the function at the current
+ program counter, evaluated against the current thread's machine state at the current snapshot.
+ Variables are gathered from two sources: the Static Listing and the Decompiler.
+
+
The variable viewer uses colors to hint about changes and freshness of displayed values. By
+ default, changed values are displayed in red, and stale values are displayed in dark gray. A
+ stale value is one whose backing register or memory location has not been recorded in
+ the current snap.
+
+
Table Columns
+
+
The table displays and allows modification of each variable. It has the following
+ columns:
+
+
+
Source (hidden by default) - indicates where the variable was discovered. The
+ value is either the Static
+ Listing or the Decompiler.
+ When both sources produce a variable with the same symbol name, only one entry is shown; the
+ Listing entry takes precedence.
+
+
Storage - the location where the variable's value is stored. For register-backed
+ variables this shows the register name. For memory-backed variables this shows the address.
+ If the storage location could not be determined, ?? is shown.
+
+
Symbol - the name of the variable. This field is user-modifiable when Enable Edits is on.
+
+
Value - the raw bytes of the variable's current value. For scalar variables whose
+ size does not exceed the pointer size, the value is shown as a hexadecimal integer. For
+ larger variables the bytes are shown as a space-separated hex sequence enclosed in braces.
+ Very large values are truncated with an ellipsis. This field is user-modifiable when Enable Edits is on and the variable's storage is writable. Edits may be
+ directed toward a live target, the trace, or the emulator, according to the Control and Machine State
+ Plugin.
+
+
Type - the data type assigned to the variable. This field is user-modifiable when
+ Enable Edits is on. The type is used to compute the Repr
+ column.
+
+
Repr - the value of the variable as interpreted by its assigned data type. This
+ field is user-modifiable when the Value column is modifiable and the selected data
+ type provides an encoder. Editing this field encodes the entered representation back to raw
+ bytes and writes them to the variable's storage location.
+
+
Error - if an error occurs while evaluating the variable (for example, because the
+ storage location is not accessible at the current snapshot, or the stack frame could not be
+ unwound), the error message is shown here.
+
+
+
Actions
+
+
The variable viewer provides the following actions:
+
+
Enable Edits
+
+
This toggle enables writes to the machine state. To modify a variable's symbol, value, type,
+ or representation, this toggle must be enabled. It is available only when there is an active
+ trace. Edits are directed according to the Control and Machine State
+ Plugin.
+
+
Show/Hide
+ Variables
+
+
This action controls which source of variables is displayed in the table. The three choices
+ are:
+
+
+
Show Variables From Both - displays
+ variables gathered from both the Listing and the Decompiler. When both sources produce a
+ variable with the same symbol name, only one entry is retained.
+
+
Show Listing Variables Only -
+ displays only variables derived from the static listing.
+
+
Show Decompiler Variables Only -
+ displays only variables recovered by the decompiler.
+
+
+
Right Click Actions
+
+
+
+
+
+
+
View [variable] @ 0x... in... - This action is available when a single variable is
+ selected and its storage address is a non-register memory address. It navigates to that
+ address in a chosen listing window.
+
+
Deref *[variable] @ 0x... in... - This action is available when a single variable
+ is selected and its current value can be interpreted as a valid memory address (i.e., the
+ variable is a pointer). It navigates to the dereferenced address in a chosen listing
+ window.
+
+
+
Sub Menu Options
+
+
Both actions share the same sub options of which listing window to show the result in:
+
+
+
Main Listing - navigates the primary dynamic listing to the variable's storage
+ address.
+
+
[Additional Listing Windows] - navigates any other open dynamic listing windows to
+ the variable's storage address.
+
+
New Listing - opens a new dynamic listing window, sets it to follow the current
+ thread, and navigates it to the variable's storage address.
+
+
+
diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerVariableViewerPlugin/images/DebuggerVariableViewerPlugin.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerVariableViewerPlugin/images/DebuggerVariableViewerPlugin.png
new file mode 100644
index 0000000000..894120f491
Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerVariableViewerPlugin/images/DebuggerVariableViewerPlugin.png differ
diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerVariableViewerPlugin/images/DebuggerVariableViewerPluginRightClick.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerVariableViewerPlugin/images/DebuggerVariableViewerPluginRightClick.png
new file mode 100644
index 0000000000..099a98a696
Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerVariableViewerPlugin/images/DebuggerVariableViewerPluginRightClick.png differ
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/variable/DebuggerVariableViewerProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/variable/DebuggerVariableViewerProvider.java
index a741546a11..7022e741fc 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/variable/DebuggerVariableViewerProvider.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/variable/DebuggerVariableViewerProvider.java
@@ -80,14 +80,13 @@ public class DebuggerVariableViewerProvider extends ComponentProviderAdapter
interface DebuggerVariableViewerPopupAction {
String NAME = "Debugger variable viewer Popup Actions";
String DESCRIPTION = "Popup actions for debugger variable viewer";
- String HELP_ANCHOR = "";
+ String HELP_ANCHOR = "popup_actions";
String GROUP1 = "z";
String GROUP2 = "zz";
- static ActionBuilder builder(ComponentProvider owner, String subgroup, String... path) {
- final String ownerName = owner.getName();
- return new ActionBuilder(NAME, ownerName).description(DESCRIPTION)
- .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR))
+ static ActionBuilder builder(String subgroup, String... path) {
+ return new ActionBuilder(NAME, "DebuggerVariableViewerPlugin").description(DESCRIPTION)
+ .helpLocation(new HelpLocation("DebuggerVariableViewerPlugin", HELP_ANCHOR))
.popupMenuGroup(GROUP1, subgroup)
.popupMenuPath(path)
.popupWhen(c -> true)
@@ -142,6 +141,8 @@ public class DebuggerVariableViewerProvider extends ComponentProviderAdapter
public DebuggerVariableViewerProvider(DebuggerVariableViewerPlugin plugin) {
super(plugin.getTool(), "Variable Viewer", plugin.getName());
+ setHelpLocation(new HelpLocation("DebuggerVariableViewerPlugin", "plugin"));
+
autoServiceWiring = AutoService.wireServicesConsumed(plugin, this);
pluginTool = plugin.getTool();
model = new DebuggerVariableViewerModel(plugin.getTool(), this);
@@ -174,6 +175,7 @@ public class DebuggerVariableViewerProvider extends ComponentProviderAdapter
c -> currentCoordinates != null && currentCoordinates.getTrace() != null)
.onAction(c -> {
})
+ .helpLocation(new HelpLocation("DebuggerVariableViewerPlugin", "enable_edits"))
.buildAndInstallLocal(this);
actionShowVariables =
@@ -188,6 +190,7 @@ public class DebuggerVariableViewerProvider extends ComponentProviderAdapter
new GIcon("icon.decompiler.action.provider"),
VariableViewerStates.DECOMPILER)
.toolBarGroup("z")
+ .helpLocation(new HelpLocation("DebuggerVariableViewerPlugin", "show_variables"))
.buildAndInstallLocal(this);
setVisible(true);
@@ -394,7 +397,7 @@ public class DebuggerVariableViewerProvider extends ComponentProviderAdapter
.getAddressFactory()
.getAddress(selected.getFirst().getValue());
if (address != null) {
- addActions(result, "View *%s @ 0x%x in...".formatted(selected.getFirst().getSymbol(),
+ addActions(result, "Deref *%s @ 0x%x in...".formatted(selected.getFirst().getSymbol(),
address.getOffset()), address);
}
return result;
@@ -403,7 +406,7 @@ public class DebuggerVariableViewerProvider extends ComponentProviderAdapter
private void addActions(List result, String name, Address address) {
// Add action for each additional debugger listing window
int i = 0;
- result.add(DebuggerVariableViewerPopupAction.builder(this, Integer.toString(i), name,
+ result.add(DebuggerVariableViewerPopupAction.builder(Integer.toString(i), name,
"Main Listing").onAction(ctx -> {
if (listingService == null) {
return;
@@ -420,7 +423,7 @@ public class DebuggerVariableViewerProvider extends ComponentProviderAdapter
continue;
}
- result.add(DebuggerVariableViewerPopupAction.builder(this, Integer.toString(i), name,
+ result.add(DebuggerVariableViewerPopupAction.builder(Integer.toString(i), name,
dl.getTitle()).onAction(ctx -> {
if (listingService == null) {
return;
@@ -433,8 +436,8 @@ public class DebuggerVariableViewerProvider extends ComponentProviderAdapter
i++;
}
- result.add(DebuggerVariableViewerPopupAction.builder(this, Integer.toString(i), name,
- "New Listing")
+ result.add(
+ DebuggerVariableViewerPopupAction.builder(Integer.toString(i), name, "New Listing")
.popupMenuGroup(DebuggerVariableViewerPopupAction.GROUP2)
.onAction(ctx -> {
if (listingService == null) {
diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/variable/DebuggerVariableViewerPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/variable/DebuggerVariableViewerPluginScreenShots.java
new file mode 100644
index 0000000000..a3aee5bf21
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/variable/DebuggerVariableViewerPluginScreenShots.java
@@ -0,0 +1,251 @@
+/* ###
+ * 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.variable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+import db.Transaction;
+import generic.test.category.NightlyCategory;
+import ghidra.app.plugin.core.debug.gui.stack.vars.VariableValueHoverPlugin;
+import ghidra.app.plugin.core.debug.service.emulation.ProgramEmulationUtils;
+import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingServicePlugin;
+import ghidra.app.plugin.core.debug.service.progress.ProgressServicePlugin;
+import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin;
+import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin;
+import ghidra.app.services.ProgressService;
+import ghidra.async.AsyncTestUtils;
+import ghidra.debug.api.progress.MonitorReceiver;
+import ghidra.framework.model.DomainFolder;
+import ghidra.framework.model.DomainObject;
+import ghidra.program.model.data.*;
+import ghidra.program.model.lang.Register;
+import ghidra.program.model.lang.RegisterValue;
+import ghidra.program.model.listing.*;
+import ghidra.program.model.symbol.SourceType;
+import ghidra.test.ToyProgramBuilder;
+import ghidra.trace.database.ToyDBTraceBuilder;
+import ghidra.trace.model.Lifespan;
+import ghidra.trace.model.memory.TraceMemoryFlag;
+import ghidra.trace.model.memory.TraceMemorySpace;
+import ghidra.trace.model.thread.TraceThread;
+import ghidra.trace.model.time.TraceSnapshot;
+import ghidra.util.InvalidNameException;
+import ghidra.util.exception.AssertException;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.ConsoleTaskMonitor;
+import ghidra.util.task.TaskMonitor;
+import help.screenshot.GhidraScreenShotGenerator;
+import org.junit.*;
+import org.junit.experimental.categories.Category;
+
+@Category(NightlyCategory.class) // this may actually be an @PortSensitive test
+public class DebuggerVariableViewerPluginScreenShots extends GhidraScreenShotGenerator
+ implements AsyncTestUtils {
+
+ private static final TaskMonitor MONITOR = new ConsoleTaskMonitor();
+ ToyDBTraceBuilder tb;
+ Program progHw;
+ private ProgramManagerPlugin programManager;
+ private DebuggerTraceManagerServicePlugin traceManager;
+ private DebuggerVariableViewerProvider provider;
+ private DebuggerStaticMappingServicePlugin mappingService;
+ private ProgressService progressService;
+
+ @Before
+ public void setUpMine() throws Exception {
+ programManager = addPlugin(tool, ProgramManagerPlugin.class);
+ traceManager = addPlugin(tool, DebuggerTraceManagerServicePlugin.class);
+ mappingService = addPlugin(tool, DebuggerStaticMappingServicePlugin.class);
+ progressService = addPlugin(tool, ProgressServicePlugin.class);
+
+ addPlugin(tool, DebuggerVariableViewerPlugin.class);
+
+ // The unwinder has a dependency on this plugin so it's required
+ addPlugin(tool, VariableValueHoverPlugin.class);
+
+ provider = waitForComponentProvider(DebuggerVariableViewerProvider.class);
+
+ populateTraceAndPrograms();
+ }
+
+ private void populateTraceAndPrograms() throws Exception {
+ ToyProgramBuilder helloworldBuilder = new ToyProgramBuilder("helloworld", false, this);
+ helloworldBuilder.tx(() -> {
+ helloworldBuilder.createMemory(".text", "0x00400000", 0x50);
+ Function function =
+ helloworldBuilder.createEmptyFunction("main", null, "__fastcall", false,
+ "0x00400000", 6, IntegerDataType.dataType,
+ new ParameterImpl("argc", IntegerDataType.dataType,
+ helloworldBuilder.getProgram()),
+ new ParameterImpl("argv", new PointerDataType(CharDataType.dataType),
+ helloworldBuilder.getProgram()),
+ new ParameterImpl("float_arg", FloatDataType.dataType,
+ helloworldBuilder.getProgram()));
+
+ progHw = helloworldBuilder.getProgram();
+
+ helloworldBuilder.createReturnInstruction("0x00400000");
+
+ function.addLocalVariable(
+ new LocalVariableImpl("StackVar", IntegerDataType.dataType, 4, progHw),
+ SourceType.USER_DEFINED);
+ });
+
+ tb = new ToyDBTraceBuilder("toy", progHw.getLanguage().getLanguageID().getIdAsString());
+
+ intoProject(progHw);
+ intoProject(tb.trace);
+ programManager.openProgram(progHw);
+
+ traceManager.openTrace(tb.trace);
+ mappingService.changesSettled().get(1, TimeUnit.SECONDS);
+
+ try (Transaction tx = tb.startTransaction()) {
+ tb.trace.getObjectManager().createRootObject(ProgramEmulationUtils.EMU_SESSION_SCHEMA);
+
+ tb.trace.getModuleManager()
+ .addLoadedModule("Modules[helloword]", "helloworld",
+ tb.range(0x00400000, 0x00400050), 0);
+
+ tb.trace.getMemoryManager()
+ .addRegion("Memory[ALL]", Lifespan.nowOn(0),
+ tb.range(0x0, 0xFFFF_FFFF_FFFF_FFFFL), TraceMemoryFlag.READ,
+ TraceMemoryFlag.EXECUTE, TraceMemoryFlag.WRITE);
+
+ mappingService.addIdentityMapping(tb.trace, progHw, Lifespan.nowOn(0), true);
+
+ TraceThread thread = tb.getOrAddThread("Threads[1]", 0);
+ tb.createObjectsRegsForThread(thread, Lifespan.nowOn(0), tb.host);
+ TraceMemorySpace regs =
+ tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, true);
+ Register pc = tb.trace.getBaseLanguage().getProgramCounter();
+ Register arg1 = tb.trace.getBaseLanguage().getRegister("r12");
+ Register arg2 = tb.trace.getBaseLanguage().getRegister("r11");
+ Register arg3 = tb.trace.getBaseLanguage().getRegister("r10");
+ Register sp = tb.trace.getBaseLanguage().getRegister("sp");
+
+ TraceSnapshot snap = tb.trace.getTimeManager().createSnapshot("Start main");
+ snap.setEventThread(thread);
+
+ regs.setValue(snap.getKey(), new RegisterValue(pc, BigInteger.valueOf(0x00400000)));
+ regs.setValue(snap.getKey(), new RegisterValue(arg1, BigInteger.valueOf(0x3)));
+ regs.setValue(snap.getKey(), new RegisterValue(arg2, BigInteger.valueOf(0xdeadbeefL)));
+ regs.setValue(snap.getKey(),
+ new RegisterValue(arg3, BigInteger.valueOf(Float.floatToIntBits(1.5f))));
+ regs.setValue(snap.getKey(), new RegisterValue(sp, BigInteger.valueOf(0x00200000)));
+
+ tb.trace.getStackManager()
+ .getStack(thread, snap.getKey(), true)
+ .getFrame(snap.getKey(), 0, true)
+ .setProgramCounter(Lifespan.at(snap.getKey()), tb.addr(0x00400000));
+
+ tb.trace.getMemoryManager()
+ .putBytes(snap.getKey(), tb.addr(0x00200004),
+ ByteBuffer.wrap(new byte[] { 0x21, 0x43, 0x65, (byte) 0x87 }));
+ }
+
+ mappingService.changesSettled().get(1, TimeUnit.SECONDS);
+
+ traceManager.activateTrace(tb.trace);
+ traceManager.activateSnap(0);
+ }
+
+ protected void intoProject(DomainObject obj) {
+ waitForDomainObject(obj);
+ DomainFolder rootFolder = tool.getProject().getProjectData().getRootFolder();
+ waitForCondition(() -> {
+ try {
+ rootFolder.createFile(obj.getName(), obj, MONITOR);
+ return true;
+ }
+ catch (InvalidNameException | CancelledException e) {
+ throw new AssertionError(e);
+ }
+ catch (IOException e) {
+ // Usually "object is busy". Try again.
+ return false;
+ }
+ });
+ }
+
+ public static void waitForDomainObject(DomainObject object) {
+ object.flushEvents();
+ waitForSwing();
+ }
+
+ @After
+ public void tearDownMine() {
+ tb.close();
+
+ if (progHw != null) {
+ progHw.release(this);
+ progHw = null;
+ }
+ }
+
+ @Test
+ public void testCaptureDebuggerVariableViewerPlugin() throws InterruptedException {
+ for (MonitorReceiver e : progressService.getAllMonitors()) {
+ waitFor(() -> !e.isValid());
+ }
+ JTable table = findComponent(provider.getComponent(), JTable.class);
+ Thread.sleep(2000);
+ waitFor(() -> table.getRowCount() > 0);
+ captureIsolatedProvider(provider, 500, 500);
+ }
+
+ @Test
+ public void testCaptureDebuggerVariableViewerPluginRightClick() throws InterruptedException {
+ for (MonitorReceiver e : progressService.getAllMonitors()) {
+ waitFor(() -> !e.isValid());
+ }
+ runSwing(() -> {
+ Window window = tool.getWindowManager().getProviderWindow(provider);
+ if (window == null) {
+ throw new AssertException(
+ "Could not find window for " + "provider--is it showing?: " +
+ provider.getName());
+ }
+
+ window.setSize(new Dimension(500, 500));
+ window.toFront();
+ provider.getComponent().requestFocus();
+ paintFix(window);
+ });
+
+ waitForSwing();
+ JTable table = findComponent(provider.getComponent(), JTable.class);
+ Thread.sleep(2000);
+ waitFor(() -> table.getRowCount() > 0);
+ selectRow(table, 0);
+ Rectangle cellRect = table.getCellRect(0, 0, true);
+ rightClick(table, cellRect.x + cellRect.width / 2, cellRect.y + cellRect.height / 2);
+ JPopupMenu popupMenu = getPopupMenu();
+ MenuElement menuElement =
+ Arrays.stream(popupMenu.getSubElements()).findFirst().orElseThrow();
+ postEvent(new MouseEvent(menuElement.getComponent(), MouseEvent.MOUSE_ENTERED,
+ System.currentTimeMillis(), 0, menuElement.getComponent().getWidth(), 0, 0,
+ false));
+ captureProviderWithScreenShot(provider);
+ }
+}