GP-6993: Add help docs for DebuggerVariableViewerPlugin

This commit is contained in:
ghidra_red
2026-08-27 17:50:22 +00:00
parent 52bb03d247
commit a8e6ca86f6
7 changed files with 406 additions and 10 deletions

View File

@@ -68,6 +68,9 @@ src/main/help/help/topics/DebuggerTraceManagerServicePlugin/DebuggerTraceManager
src/main/help/help/topics/DebuggerTraceViewDiffPlugin/DebuggerTraceViewDiffPlugin.html||GHIDRA||||END|
src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTimeSelectionDialog.png||GHIDRA||||END|
src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTraceViewDiffPlugin.png||GHIDRA||||END|
src/main/help/help/topics/DebuggerVariableViewerPlugin/DebuggerVariableViewerPlugin.html||GHIDRA||||END|
src/main/help/help/topics/DebuggerVariableViewerPlugin/images/DebuggerVariableViewerPlugin.png||GHIDRA||||END|
src/main/help/help/topics/DebuggerVariableViewerPlugin/images/DebuggerVariableViewerPluginRightClick.png||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/help/help/topics/DynamicStaticSynchronizationPlugin/DynamicStaticSynchronizationPlugin.html||GHIDRA||||END|

View File

@@ -130,6 +130,10 @@
<tocdef id="DebuggerPlatformPlugin" text="Platform Selection"
sortgroup="y"
target="help/topics/DebuggerPlatformPlugin/DebuggerPlatformPlugin.html" />
<tocdef id="DebuggerVariableViewerPlugin" text="Variable Viewer"
sortgroup="za"
target="help/topics/DebuggerVariableViewerPlugin/DebuggerVariableViewerPlugin.html" />
</tocdef>
</tocref>
</tocroot>

View File

@@ -0,0 +1,135 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML>
<HEAD>
<META name="generator" content=
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
<TITLE>Debugger: Variable Viewer</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<BODY lang="EN-US">
<H1><A name="plugin"></A>Debugger: Variable Viewer</H1>
<DIV class="image">
<IMG alt="" src="images/DebuggerVariableViewerPlugin.png">
</DIV>
<P>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 <A href=
"help/topics/CodeBrowserPlugin/CodeBrowser.htm">Static Listing</A> and the <A href=
"help/topics/DecompilePlugin/DecompilerIntro.html">Decompiler</A>.</P>
<P>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
<EM>stale</EM> value is one whose backing register or memory location has not been recorded in
the current snap.</P>
<H2>Table Columns</H2>
<P>The table displays and allows modification of each variable. It has the following
columns:</P>
<UL>
<LI><B>Source</B> (hidden by default) - indicates where the variable was discovered. The
value is either the <A href="help/topics/CodeBrowserPlugin/CodeBrowser.htm">Static
Listing</A> or the <A href="help/topics/DecompilePlugin/DecompilerIntro.html">Decompiler</A>.
When both sources produce a variable with the same symbol name, only one entry is shown; the
Listing entry takes precedence.</LI>
<LI><B>Storage</B> - 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, <CODE>??</CODE> is shown.</LI>
<LI><B>Symbol</B> - the name of the variable. This field is user-modifiable when <A href=
"#enable_edits">Enable Edits</A> is on.</LI>
<LI><B>Value</B> - 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 <A href=
"#enable_edits">Enable Edits</A> 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 <A href=
"help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html">Control and Machine State
Plugin</A>.</LI>
<LI><B>Type</B> - the data type assigned to the variable. This field is user-modifiable when
<A href="#enable_edits">Enable Edits</A> is on. The type is used to compute the <B>Repr</B>
column.</LI>
<LI><B>Repr</B> - the value of the variable as interpreted by its assigned data type. This
field is user-modifiable when the <B>Value</B> 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.</LI>
<LI><B>Error</B> - 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.</LI>
</UL>
<H2>Actions</H2>
<P>The variable viewer provides the following actions:</P>
<H3><A name="enable_edits"></A><IMG alt="" src="icon.debugger.enable.edits">Enable Edits</H3>
<P>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 <A href=
"help/topics/DebuggerControlPlugin/DebuggerControlPlugin.html">Control and Machine State
Plugin</A>.</P>
<H3><A name="show_variables"></A><IMG alt="" src="icon.configure.filter">Show/Hide
Variables</H3>
<P>This action controls which source of variables is displayed in the table. The three choices
are:</P>
<UL>
<LI><IMG alt="" src="icon.configure.filter"><B>Show Variables From Both</B> - 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.</LI>
<LI><IMG alt="" src="icon.plugin.codebrowser.provider"><B>Show Listing Variables Only</B> -
displays only variables derived from the static listing.</LI>
<LI><IMG alt="" src="icon.decompiler.action.provider"><B>Show Decompiler Variables Only</B> -
displays only variables recovered by the decompiler.</LI>
</UL>
<H3><A name="popup_actions"></A>Right Click Actions</H3>
<DIV class="image">
<IMG alt="" src="images/DebuggerVariableViewerPluginRightClick.png">
</DIV>
<UL>
<LI><B>View [variable] @ 0x... in...</B> - 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.</LI>
<LI><B>Deref *[variable] @ 0x... in...</B> - 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.</LI>
</UL>
<H4>Sub Menu Options</H4>
<P>Both actions share the same sub options of which listing window to show the result in:</P>
<UL>
<LI><B>Main Listing</B> - navigates the primary dynamic listing to the variable's storage
address.</LI>
<LI><B>[Additional Listing Windows]</B> - navigates any other open dynamic listing windows to
the variable's storage address.</LI>
<LI><B>New Listing</B> - opens a new dynamic listing window, sets it to follow the current
thread, and navigates it to the variable's storage address.</LI>
</UL>
</BODY>
</HTML>

View File

@@ -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<DockingActionIf> 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) {

View File

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