diff --git a/Ghidra/Debug/Debugger/certification.manifest b/Ghidra/Debug/Debugger/certification.manifest index e90f410858..4d989a1b13 100644 --- a/Ghidra/Debug/Debugger/certification.manifest +++ b/Ghidra/Debug/Debugger/certification.manifest @@ -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| 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 ff425d4024..e3ed917984 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml @@ -130,6 +130,10 @@ + + diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerVariableViewerPlugin/DebuggerVariableViewerPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerVariableViewerPlugin/DebuggerVariableViewerPlugin.html new file mode 100644 index 0000000000..377cc5f9c5 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerVariableViewerPlugin/DebuggerVariableViewerPlugin.html @@ -0,0 +1,135 @@ + + + + + + + Debugger: Variable Viewer + + + + + +

Debugger: Variable Viewer

+ +
+ +
+ +

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:

+ + + +

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:

+ + + +

Right Click Actions

+ +
+ +
+ + + +

Sub Menu Options

+ +

Both actions share the same sub options of which listing window to show the result in:

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