diff --git a/Ghidra/Debug/Debugger/certification.manifest b/Ghidra/Debug/Debugger/certification.manifest index fa575a47f0..b9baa1875d 100644 --- a/Ghidra/Debug/Debugger/certification.manifest +++ b/Ghidra/Debug/Debugger/certification.manifest @@ -68,6 +68,7 @@ src/main/help/help/topics/DebuggerRegistersPlugin/images/DebuggerAvailableRegist src/main/help/help/topics/DebuggerRegistersPlugin/images/DebuggerRegistersPlugin.png||GHIDRA||||END| src/main/help/help/topics/DebuggerStackPlugin/DebuggerStackPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerStackPlugin/images/DebuggerStackPlugin.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerStackPlugin/images/DebuggerStackUnwindInListing.png||GHIDRA||||END| src/main/help/help/topics/DebuggerStaticMappingPlugin/DebuggerStaticMappingPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerStaticMappingPlugin/images/DebuggerStaticMappingPlugin.png||GHIDRA||||END| src/main/help/help/topics/DebuggerTargetsPlugin/DebuggerTargetsPlugin.html||GHIDRA||||END| @@ -83,6 +84,10 @@ src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTimeSelecti src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTraceViewDiffPlugin.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/VariableValueHoverPlugin/VariableValueHoverPlugin.html||GHIDRA||||END| +src/main/help/help/topics/VariableValueHoverPlugin/images/VariableValueHoverPluginBrowser.png||GHIDRA||||END| +src/main/help/help/topics/VariableValueHoverPlugin/images/VariableValueHoverPluginDecompiler.png||GHIDRA||||END| +src/main/help/help/topics/VariableValueHoverPlugin/images/VariableValueHoverPluginListing.png||GHIDRA||||END| src/main/resources/defaultTools/Debugger.tool||GHIDRA||||END| src/main/resources/images/add.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| src/main/resources/images/attach.png||GHIDRA||||END| diff --git a/Ghidra/Debug/Debugger/ghidra_scripts/ComputeUnwindInfoScript.java b/Ghidra/Debug/Debugger/ghidra_scripts/ComputeUnwindInfoScript.java new file mode 100644 index 0000000000..b02b3bdd67 --- /dev/null +++ b/Ghidra/Debug/Debugger/ghidra_scripts/ComputeUnwindInfoScript.java @@ -0,0 +1,64 @@ +/* ### + * 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. + */ +//A script to analyze unwind information for the current function wrt. the current location +//as a program counter. The resulting information can be used to interpret how the function +//is using various elements on the stack when the program counter is at the cursor. This +//script is more for diagnostic and demonstration purposes, since the application of unwind +//information is already integrated into the Debugger. +//@author +//@category Stack +//@keybinding +//@menupath +//@toolbar + +import java.util.Map.Entry; + +import ghidra.app.plugin.core.debug.stack.*; +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.Register; + +public class ComputeUnwindInfoScript extends GhidraScript { + + String addressToString(Address address) { + Register[] registers = currentProgram.getLanguage().getRegisters(address); + if (registers.length == 0) { + return address.toString(); + } + return registers[0].getBaseRegister().toString(); + } + + @Override + protected void run() throws Exception { + UnwindAnalysis ua = new UnwindAnalysis(currentProgram); + UnwindInfo info = ua.computeUnwindInfo(currentAddress, monitor); + + if (info == null) { + println("Could not unwind"); + return; + } + println("Stack depth at " + currentAddress + ": " + info.depth()); + println("Return address address: " + addressToString(info.ofReturn())); + println("Saved registers:"); + for (Entry entry : info.saved().entrySet()) { + println(" " + entry); + } + println("Warnings:"); + for (StackUnwindWarning warning : info.warnings()) { + println(" " + warning.getMessage()); + } + } +} diff --git a/Ghidra/Debug/Debugger/ghidra_scripts/EmuDeskCheckScript.java b/Ghidra/Debug/Debugger/ghidra_scripts/EmuDeskCheckScript.java index 2487297a66..811eed3ef3 100644 --- a/Ghidra/Debug/Debugger/ghidra_scripts/EmuDeskCheckScript.java +++ b/Ghidra/Debug/Debugger/ghidra_scripts/EmuDeskCheckScript.java @@ -143,11 +143,11 @@ public class EmuDeskCheckScript extends GhidraScript implements FlatDebuggerAPI protected BytesPcodeThread createThread(String name) { return new BytesPcodeThread(name, this) { TraceThread thread = trace.getThreadManager().getLiveThreadByPath(snap, name); - PcodeExecutor> inspector = + PcodeExecutor> inspector = new PcodeExecutor<>(language, new PairedPcodeArithmetic<>(arithmetic, - AddressOfPcodeArithmetic.INSTANCE), - state.paired(new AddressOfPcodeExecutorStatePiece(language)), + LocationPcodeArithmetic.forEndian(language.isBigEndian())), + state.paired(new LocationPcodeExecutorStatePiece(language)), Reason.INSPECT); { @@ -169,7 +169,7 @@ public class EmuDeskCheckScript extends GhidraScript implements FlatDebuggerAPI } public CheckRow createRow() { - List> values = new ArrayList<>(); + List> values = new ArrayList<>(); for (PcodeExpression exp : compiled) { values.add(exp.evaluate(inspector)); } @@ -254,17 +254,17 @@ public class EmuDeskCheckScript extends GhidraScript implements FlatDebuggerAPI class CheckRow implements AddressableRowObject { private final TraceSchedule schedule; private final Address pc; - private final List> values; + private final List> values; public CheckRow(TraceSchedule schedule, Address pc, - List> values) { + List> values) { this.schedule = schedule; this.pc = pc; this.values = values; } @Override - public Address getAddress() { + public Address getAddress() { // Instruction address TraceProgramView view = getCurrentView(); if (view == null) { return Address.NO_ADDRESS; @@ -352,8 +352,8 @@ public class EmuDeskCheckScript extends GhidraScript implements FlatDebuggerAPI private Object getObjectValue(CheckRow r) { try { - Pair p = r.values.get(index); - Address addr = p.getRight(); + Pair p = r.values.get(index); + Address addr = p.getRight() == null ? null : p.getRight().getAddress(); byte[] bytes = p.getLeft(); return type.getValue(new ByteMemBufferImpl(addr, bytes, isBigEndian), watch.settings, bytes.length); @@ -365,8 +365,8 @@ public class EmuDeskCheckScript extends GhidraScript implements FlatDebuggerAPI private String getStringValue(CheckRow r) { try { - Pair p = r.values.get(index); - Address addr = p.getRight(); + Pair p = r.values.get(index); + Address addr = p.getRight() == null ? null : p.getRight().getAddress(); byte[] bytes = p.getLeft(); return type.getRepresentation(new ByteMemBufferImpl(addr, bytes, isBigEndian), watch.settings, bytes.length); 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 a34ed2bfe5..1045bbc448 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml @@ -153,8 +153,12 @@ sortgroup="n" target="help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html" /> - + + Comment - a user-modifiable comment. + +

Action

+ +

The stack plugin provides a single action:

+ +

Unwind Stack (U)

+ +

This action is in the main menu: Debugger → Analysis → Unwind + from frame 0. It attempts to unwind the current thread's stack segment, creating frame + data units in the listing. It starts by reading the program counter and stack pointer from the + innermost frame of the current thread. It then maps the program counter to the program database + and analyzes the function containing it. If successful, it can determine the frame's base + address and locate variables, saved registers, and the return address. Knowing the return + address and frame depth, it can derive the program counter and stack pointer of the next frame + and unwind it in the same manner. This proceeds until analysis fails or the stack segment is + exhausted. For best results, ensure you have imported and opened the Ghidra program database + for every module, or at least the subset you expect to see in your stack. To view the results, + navigate to or follow the stack pointer in a Dynamic Listing.

+ + + + + + + +
+ +

Each call record generates a structure data unit derived from the function's frame. The + exact contents of the structure depend on the current program counter within that function. + Only those entries actually allocated at the program counter are included. Each field in that + structure can be one of five kinds:

+ +
    +
  • Local stack variable: These are named with the local_ prefix. They + correspond exactly to those entries found in the function's stack frame in the program + database.
  • + +
  • Stack parameter: These are named with the param_ prefix. They + correspond exactly to those entries found in the function's stack frame in the program + database.
  • + +
  • Return address: This is named return_address. It is determined by + interpreting the function's machine code.
  • + +
  • Saved register: These are named with the saved_ prefix. They are + determined by interpreting the function's machine code.
  • + +
  • Slack space: These are named with the offset_ prefix (or + posOff_ for positive offsets). They represent unused or unknown entries.
  • +
+ +

The frame entries are not automatically updated when a function's frame changes in a program + database. To update the unwind after changing a function's stack frame, you must unwind + again.

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStackPlugin/images/DebuggerStackUnwindInListing.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStackPlugin/images/DebuggerStackUnwindInListing.png new file mode 100644 index 0000000000..35b4e8f7e0 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerStackPlugin/images/DebuggerStackUnwindInListing.png differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/VariableValueHoverPlugin/VariableValueHoverPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/VariableValueHoverPlugin/VariableValueHoverPlugin.html new file mode 100644 index 0000000000..990baccb5f --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/VariableValueHoverPlugin/VariableValueHoverPlugin.html @@ -0,0 +1,141 @@ + + + + + + + Debugger: Variable Hovers + + + + + +

Debugger: Variable Hovers

+ +

This service plugin provides hovers to the Static Listing, the Dynamic Listing, and the Decompiler. Hovering the mouse over + variables or operands in any of those windows will cause the service to display a tip showing + the value of that variable, if it can. For stack and register variables, the service will + attempt to unwind the stack until it finds a call record for the function using the variable. + Thus, it is important to have all the modules imported, analyzed, and open in the Debugger so + that the unwinder can access the static analysis of every function it needs to unwind.

+ +

Stack unwinding can be tenuous. It relies heavily on accurate static analysis and expects + functions to follow certain conventions. Thus, it's very easy to break and may frequently be + incorrect. For hovers that include a Frame row, the displayed value depends on an + accurately unwound stack. Take the value with a grain of salt, especially if the hover also + includes a Warnings: row. To diagnose the unwound stack, use the Unwind Stack + action.

+ +

Table Rows

+ +

A hover may display any of the following rows:

+ +
    +
  • Name: The name of the variable or operand.
  • + +
  • Frame: A description of the frame (call record) unwound to evaluate the variable. + This is omitted for global or static variables and for raw register operands.
  • + +
  • Storage: The statically-defined storage of the variable.
  • + +
  • Type: The data type of the variable.
  • + +
  • Instruction: If the operand refers to code, the instruction at the target + address.
  • + +
  • Location: The actual location of the variable on the target.
  • + +
  • Bytes: The bytes in the variable, subject to the target's endianness. For long + buffers, the bytes are split into lines of 16 bytes each for at most 16 lines.
  • + +
  • Integer: The value displayed as an integer in various formats: decimal, + hexadecimal; unsigned, signed. The alternative formats are only included if they differ from + the formats already presented.
  • + +
  • Value: The value as given by its type's default representation. This only applies + if the variable has a type and it was able to interpret the variable's bytes.
  • + +
  • Status: If the evaluation is taking significant time, this provides feedback while + evaluation proceeds in the background.
  • + +
  • Warnings: Displays any warnings encountered while unwinding the stack, if + applicable.
  • + +
  • Error: Displays an exception or error message when there was a problem evaluating + the variable. Other rows may be present, but overall the table is incomplete.
  • +
+ +

Examples

+ + + + + + + + + + + +
A register operand in the Dynamic Listing
+ +

When hovering over operands in the Dynamic Listing, that operand is most likely a register. + The register's value is displayed without regard to the stack frame. It will always use the + "innermost frame," meaning it will display the register's current value. In the example, the + user has hovered over register EDX; however, the value of EDX was not recorded, so its integer + value 0 is displayed in gray. Furthermore, the user had not assigned a type to EDX + in the Registers + window, and so the service cannot interpret the value except as an integer. Register values are + never displayed as raw byte arrays.

+ + + + + + + + + + + +
A stack variable in the Static Listing
+ +

When hovering over operands in the Static Listing, the service will gather context about the + operand and find a frame for the relevant function. It will take the first appropriate frame it + encounters during unwinding (from innermost out) so long as the frame's level is at least the + current frame level. In the example, the user has hovered over the parameter n, which + is a stack variable of the function fib. The curent frame is 0, so the service unwinds + the stack, finds that the current frame is a call record for fib, and selects it. It + displays the variable's static storage Stack[0x4]:4 and type uint. It + then applies this information to determine the actual run-time location and value. Since the + frame base is 00004fa0, it adds the stack offset to compute the run-time location + 00004fa4:4. It reads the bytes 01 00 00 00 from the target and + computes the integer value 1. It also interprets the value using the assigned data + type, giving 1h.

+ + + + + + + + + + + +
A stack variable in the Decompiler
+ +

When hovering over variables in the Decompiler, the service behaves similarly to how it does + for operands in the Static Listing. It locates the appropriate frame and attempts to derive the + variable's run-time value. Just as in the Static Listing example above, the user has hovered + over the variable n, so the service has again computed the value 1h.

+ + diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/VariableValueHoverPlugin/images/VariableValueHoverPluginBrowser.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/VariableValueHoverPlugin/images/VariableValueHoverPluginBrowser.png new file mode 100644 index 0000000000..5c7ee4de7c Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/VariableValueHoverPlugin/images/VariableValueHoverPluginBrowser.png differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/VariableValueHoverPlugin/images/VariableValueHoverPluginDecompiler.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/VariableValueHoverPlugin/images/VariableValueHoverPluginDecompiler.png new file mode 100644 index 0000000000..0a44d28236 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/VariableValueHoverPlugin/images/VariableValueHoverPluginDecompiler.png differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/VariableValueHoverPlugin/images/VariableValueHoverPluginListing.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/VariableValueHoverPlugin/images/VariableValueHoverPluginListing.png new file mode 100644 index 0000000000..ca5c1339d4 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/VariableValueHoverPlugin/images/VariableValueHoverPluginListing.png differ diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPlugin.java index 26659b9a67..038786b375 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPlugin.java @@ -39,6 +39,7 @@ import ghidra.app.plugin.core.debug.gui.action.LocationTrackingSpec; import ghidra.app.plugin.core.debug.gui.listing.MultiBlendedListingBackgroundColorModel; import ghidra.app.plugin.core.debug.gui.time.DebuggerTimeSelectionDialog; import ghidra.app.plugin.core.debug.utils.BackgroundUtils.PluginToolExecutorService; +import ghidra.app.plugin.core.debug.utils.BackgroundUtils.PluginToolExecutorService.TaskOpt; import ghidra.app.services.*; import ghidra.app.services.DebuggerListingService.LocationTrackingSpecChangeListener; import ghidra.app.util.viewer.listingpanel.ListingPanel; @@ -58,12 +59,21 @@ import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.time.schedule.TraceSchedule; import ghidra.util.Msg; -@PluginInfo(shortDescription = "Compare memory state between times in a trace", description = "Provides a side-by-side diff view between snapshots (points in time) in a " + - "trace. The comparison is limited to raw bytes.", category = PluginCategoryNames.DEBUGGER, packageName = DebuggerPluginPackage.NAME, status = PluginStatus.RELEASED, eventsConsumed = { +@PluginInfo( + shortDescription = "Compare memory state between times in a trace", + description = "Provides a side-by-side diff view between snapshots (points in time) in a " + + "trace. The comparison is limited to raw bytes.", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, + eventsConsumed = { TraceClosedPluginEvent.class, - }, eventsProduced = {}, servicesRequired = { + }, + eventsProduced = {}, + servicesRequired = { DebuggerListingService.class, - }, servicesProvided = {}) + }, + servicesProvided = {}) public class DebuggerTraceViewDiffPlugin extends AbstractDebuggerPlugin { protected static final String MARKER_NAME = "Trace Diff"; protected static final String MARKER_DESCRIPTION = "Difference between snapshots in this trace"; @@ -223,7 +233,8 @@ public class DebuggerTraceViewDiffPlugin extends AbstractDebuggerPlugin { DebuggerCoordinates current = traceManager.getCurrent(); DebuggerCoordinates alternate = traceManager.resolveTime(time); PluginToolExecutorService toolExecutorService = - new PluginToolExecutorService(tool, "Computing diff", true, true, false, 500); + new PluginToolExecutorService(tool, "Computing diff", null, 500, + TaskOpt.HAS_PROGRESS, TaskOpt.CAN_CANCEL); return traceManager.materialize(alternate).thenApplyAsync(snap -> { clearMarkers(); TraceProgramView altView = alternate.getTrace().getFixedProgramView(snap); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memview/MemviewMapModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memview/MemviewMapModel.java index d150793213..4febfd30b6 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memview/MemviewMapModel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memview/MemviewMapModel.java @@ -109,10 +109,9 @@ class MemviewMapModel extends AbstractSortedTableModel { } /** - * Convenience method for locating columns by name. - * Implementation is naive so this should be overridden if - * this method is to be called often. This method is not - * in the TableModel interface and is not used by the JTable. + * Convenience method for locating columns by name. Implementation is naive so this should be + * overridden if this method is to be called often. This method is not in the TableModel + * interface and is not used by the JTable. */ @Override public int findColumn(String columnName) { @@ -125,7 +124,7 @@ class MemviewMapModel extends AbstractSortedTableModel { } /** - * Returns Object.class by default + * Returns Object.class by default */ @Override public Class getColumnClass(int columnIndex) { @@ -136,7 +135,7 @@ class MemviewMapModel extends AbstractSortedTableModel { } /** - * Return whether this column is editable. + * Return whether this column is editable. */ @Override public boolean isCellEditable(int rowIndex, int columnIndex) { @@ -144,10 +143,9 @@ class MemviewMapModel extends AbstractSortedTableModel { } /** - * Returns the number of records managed by the data source object. A - * JTable uses this method to determine how many rows it - * should create and display. This method should be quick, as it - * is call by JTable quite frequently. + * Returns the number of records managed by the data source object. A JTable uses this + * method to determine how many rows it should create and display. This method should be quick, + * as it is call by JTable quite frequently. * * @return the number or rows in the model * @see #getColumnCount diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java index 9c6f34fb11..153186c853 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java @@ -16,31 +16,49 @@ package ghidra.app.plugin.core.debug.gui.stack; import java.awt.BorderLayout; +import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.Objects; -import javax.swing.JComponent; -import javax.swing.JPanel; +import javax.swing.*; import org.apache.commons.lang3.ArrayUtils; import docking.ActionContext; import docking.WindowPosition; +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; 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.stack.UnwindStackCommand; import ghidra.app.services.DebuggerStaticMappingService; -import ghidra.framework.plugintool.AutoService; -import ghidra.framework.plugintool.ComponentProviderAdapter; +import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Function; import ghidra.program.util.ProgramLocation; import ghidra.trace.model.*; import ghidra.trace.model.thread.TraceThread; +import ghidra.util.HelpLocation; public class DebuggerStackProvider extends ComponentProviderAdapter { + public interface UnwindStackAction { + String NAME = "Unwind from frame 0"; + String DESCRIPTION = "Unwind the stack, placing frames in the dynamic listing"; + String HELP_ANCHOR = "unwind_stack"; + KeyStroke KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_U, 0); + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName).description(DESCRIPTION) + .menuPath(DebuggerPluginPackage.NAME, "Analysis", NAME) + .keyBinding(KEY_STROKE) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) { if (!Objects.equals(a.getTrace(), b.getTrace())) { return false; @@ -71,6 +89,8 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { /*testing*/ DebuggerStackPanel panel; /*testing*/ DebuggerLegacyStackPanel legacyPanel; + DockingAction actionUnwindStack; + public DebuggerStackProvider(DebuggerStackPlugin plugin) { super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_STACK, plugin.getName()); this.plugin = plugin; @@ -96,7 +116,14 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { } protected void createActions() { - // TODO: Anything? + actionUnwindStack = UnwindStackAction.builder(plugin) + .enabledWhen(ctx -> current.getTrace() != null) + .onAction(this::activatedUnwindStack) + .buildAndInstall(tool); + } + + private void activatedUnwindStack(ActionContext ignored) { + new UnwindStackCommand(tool, current).run(tool, current.getTrace()); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueHoverPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueHoverPlugin.java new file mode 100644 index 0000000000..1ead244f69 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueHoverPlugin.java @@ -0,0 +1,66 @@ +/* ### + * 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.stack.vars; + +import ghidra.app.decompiler.component.hover.DecompilerHoverService; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.codebrowser.hover.ListingHoverService; +import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +@PluginInfo( + status = PluginStatus.RELEASED, + packageName = DebuggerPluginPackage.NAME, + category = PluginCategoryNames.DEBUGGER, + shortDescription = "Variable Values Hover", + description = "Displays live variable values in a tooltip as you hover over a variable in " + + "the listings or decompiler.", + eventsConsumed = { + TraceClosedPluginEvent.class + }, + servicesProvided = { + ListingHoverService.class, + DecompilerHoverService.class + }) +public class VariableValueHoverPlugin extends Plugin { + private VariableValueHoverService hoverService; + + public VariableValueHoverPlugin(PluginTool tool) { + super(tool); + hoverService = new VariableValueHoverService(tool); + registerServiceProvided(ListingHoverService.class, hoverService); + registerServiceProvided(DecompilerHoverService.class, hoverService); + } + + public VariableValueHoverService getHoverService() { + return hoverService; + } + + @Override + protected void dispose() { + hoverService.dispose(); + } + + @Override + public void processEvent(PluginEvent event) { + super.processEvent(event); + if (event instanceof TraceClosedPluginEvent evt) { + hoverService.traceClosed(evt.getTrace()); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueHoverService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueHoverService.java new file mode 100644 index 0000000000..f603dda212 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueHoverService.java @@ -0,0 +1,675 @@ +/* ### + * 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.stack.vars; + +import java.awt.Window; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import javax.swing.*; + +import docking.widgets.fieldpanel.field.Field; +import docking.widgets.fieldpanel.support.FieldLocation; +import ghidra.GhidraOptions; +import ghidra.app.decompiler.ClangFieldToken; +import ghidra.app.decompiler.ClangToken; +import ghidra.app.decompiler.component.ClangTextField; +import ghidra.app.decompiler.component.hover.DataTypeDecompilerHover; +import ghidra.app.decompiler.component.hover.DecompilerHoverService; +import ghidra.app.plugin.core.codebrowser.hover.ListingHoverService; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.gui.stack.vars.VariableValueRow.*; +import ghidra.app.plugin.core.debug.gui.stack.vars.VariableValueUtils.VariableEvaluator; +import ghidra.app.plugin.core.debug.stack.*; +import ghidra.app.plugin.core.debug.utils.BackgroundUtils.PluginToolExecutorService; +import ghidra.app.plugin.core.debug.utils.BackgroundUtils.PluginToolExecutorService.TaskOpt; +import ghidra.app.plugin.core.hover.AbstractConfigurableHover; +import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.async.AsyncUtils; +import ghidra.async.SwingExecutorService; +import ghidra.framework.plugintool.AutoService; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.pcode.exec.DebuggerPcodeUtils; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValue; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValuePcodeExecutorState; +import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; +import ghidra.program.model.address.*; +import ghidra.program.model.data.DataType; +import ghidra.program.model.data.PointerDataType; +import ghidra.program.model.lang.Register; +import ghidra.program.model.listing.*; +import ghidra.program.model.pcode.*; +import ghidra.program.model.scalar.Scalar; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.Symbol; +import ghidra.program.util.*; +import ghidra.trace.model.Trace; +import ghidra.trace.model.listing.*; +import ghidra.trace.model.memory.TraceMemoryState; +import ghidra.trace.model.program.TraceProgramView; +import ghidra.util.task.TaskMonitor; + +public class VariableValueHoverService extends AbstractConfigurableHover + implements ListingHoverService, DecompilerHoverService { + private static final String NAME = "Variable Value Display"; + private static final String DESCRIPTION = + "Show a variable's value when hovering over it and debugging"; + + private static final int PRIORITY = 100; + + private static class LRUCache extends LinkedHashMap { + private static final int DEFAULT_MAX_SIZE = 5; + + private int maxSize; + + public LRUCache() { + this(DEFAULT_MAX_SIZE); + } + + public LRUCache(int maxSize) { + super(maxSize, 0.75f, true); + this.maxSize = maxSize; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > maxSize) { + removed(eldest); + return true; + } + return false; + } + + protected void removed(Map.Entry eldest) { + } + } + + // TODO: Option to always unwind from frame 0, or take the nearest frame? + + @AutoServiceConsumed + private DebuggerTraceManagerService traceManager; + @AutoServiceConsumed + private DebuggerStaticMappingService mappingService; + @SuppressWarnings("unused") + private final AutoService.Wiring autoServiceWiring; + + private final Map cachedEvaluators = + new LRUCache<>() { + protected void removed(Map.Entry eldest) { + eldest.getValue().dispose(); + } + }; + + public VariableValueHoverService(PluginTool tool) { + super(tool, PRIORITY); + autoServiceWiring = AutoService.wireServicesConsumed(tool, this); + } + + @Override + public void dispose() { + super.dispose(); + for (VariableEvaluator eval : cachedEvaluators.values()) { + eval.dispose(); + } + } + + @Override + protected String getName() { + return NAME; + } + + @Override + protected String getDescription() { + return DESCRIPTION; + } + + @Override + protected String getOptionsCategory() { + return GhidraOptions.CATEGORY_DECOMPILER_POPUPS; + } + + public static class TableFiller { + private final VariableValueTable table; + private final PluginTool tool; + private final DebuggerCoordinates current; + private final List warnings; + + private final DebuggerStaticMappingService mappingService; + private final VariableEvaluator eval; + + public TableFiller(VariableValueTable table, PluginTool tool, DebuggerCoordinates current, + VariableEvaluator eval, List warnings) { + this.table = table; + this.tool = tool; + this.current = current; + this.warnings = warnings; + + this.mappingService = tool.getService(DebuggerStaticMappingService.class); + this.eval = eval; + } + + protected CompletableFuture executeBackground( + java.util.function.Function command) { + PluginToolExecutorService executor = + new PluginToolExecutorService(tool, "Get Variable Value", current.getTrace(), 250, + TaskOpt.IS_BACKGROUND, TaskOpt.CAN_CANCEL); + return CompletableFuture.supplyAsync(() -> command.apply(executor.getLastMonitor()), + executor); + } + + public VariableValueTable fillUndefinedUnit(TraceData dynData, Program stProg, + Address stAddr) { + if (stProg == null) { + return fillDefinedData(dynData); + } + CodeUnit stUnit = stProg.getListing().getCodeUnitAt(stAddr); + if (stUnit == null) { + return fillDefinedData(dynData); + } + if (stUnit instanceof Data stData) { + DataType stType = stData.getDataType(); + if (stType == DataType.DEFAULT) { + return fillDefinedData(dynData); + } + table.add(StorageRow.fromCodeUnit(stUnit)); + table.add(new TypeRow(stType)); + AddressRange dynRange = new AddressRangeImpl(dynData.getMinAddress(), + dynData.getMinAddress().add(stData.getLength() - 1)); + table.add(LocationRow.fromRange(dynRange)); + BytesRow bytesRow = + BytesRow.fromRange(current.getPlatform(), dynRange, current.getViewSnap()); + table.add(bytesRow); + table.add(new IntegerRow(bytesRow)); + String repr = eval.getRepresentation(dynData.getAddress(), bytesRow.bytes().bytes(), + stType, stData); + if (repr != null) { + table.add(new ValueRow(repr, bytesRow.state())); + } + return table; + } + if (stUnit instanceof Instruction stIns) { + fillDefinedData(dynData); + table.add(new InstructionRow(stIns)); + table.add(new WarningsRow("Instruction taken from static listing")); + return table; + } + throw new AssertionError(); + } + + public VariableValueTable fillDefinedData(TraceData data) { + table.add(new TypeRow(data.getDataType())); + table.add(LocationRow.fromCodeUnit(data)); + BytesRow bytesRow = BytesRow.fromCodeUnit(data, current.getViewSnap()); + table.add(bytesRow); + table.add(new IntegerRow(bytesRow)); + String repr = data.getDefaultValueRepresentation(); + if (repr != null) { + table.add(new ValueRow(repr, bytesRow.state())); + } + return table; + } + + public VariableValueTable fillInstruction(TraceInstruction ins) { + table.add(LocationRow.fromCodeUnit(ins)); + table.add(BytesRow.fromCodeUnit(ins, current.getViewSnap())); + table.add(new InstructionRow(ins)); + return table; + } + + public VariableValueTable fillCodeUnit(TraceCodeUnit unit, Program stProg, Address stAddr) { + Symbol[] dynSymbols = unit.getSymbols(); + if (dynSymbols.length != 0) { + table.add(new NameRow(dynSymbols[0].getName(true))); + } + else if (stProg != null) { + Symbol[] stSymbols = stProg.getSymbolTable().getSymbols(stAddr); + if (stSymbols.length != 0) { + table.add(new NameRow(stSymbols[0].getName(true))); + } + } + + if (unit instanceof TraceData data) { + if (data.getDataType() == DataType.DEFAULT) { + return fillUndefinedUnit(data, stProg, stAddr); + } + return fillDefinedData(data); + } + else if (unit instanceof TraceInstruction ins) { + return fillInstruction(ins); + } + else { + throw new AssertionError(); + } + } + + record MappedLocation(Program stProg, Address stAddr, Address dynAddr) { + } + + protected MappedLocation mapLocation(Program programOrView, Address address) { + if (programOrView instanceof TraceProgramView view) { + ProgramLocation stLoc = + mappingService.getStaticLocationFromDynamic(new ProgramLocation(view, address)); + return stLoc == null + ? new MappedLocation(null, null, address) + : new MappedLocation(stLoc.getProgram(), stLoc.getAddress(), address); + } + ProgramLocation dynLoc = mappingService.getDynamicLocationFromStatic(current.getView(), + new ProgramLocation(programOrView, address)); + return new MappedLocation(programOrView, address, + dynLoc == null ? null : dynLoc.getAddress()); + } + + public CompletableFuture fillMemory(Program programOrView, + Address refAddress) { + MappedLocation mapped = mapLocation(programOrView, refAddress); + if (mapped.dynAddr == null) { + return null; + } + WatchValuePcodeExecutorState state = DebuggerPcodeUtils.buildWatchState(tool, current); + TraceCodeUnitsView codeUnits = current.getTrace().getCodeManager().codeUnits(); + TraceCodeUnit unit = codeUnits.getContaining(current.getViewSnap(), mapped.dynAddr); + if (unit == null) { + // Not sure this should ever happen.... + return null; + } + return CompletableFuture.supplyAsync(() -> { + state.getVar(mapped.dynAddr, unit.getLength(), true, Reason.INSPECT); + TraceCodeUnit unitAfterUpdate = + codeUnits.getContaining(current.getViewSnap(), mapped.dynAddr); + return fillCodeUnit(unitAfterUpdate, mapped.stProg, mapped.stAddr); + }); + } + + public CompletableFuture fillStack(Instruction ins, + Address stackAddress) { + Function function = + ins.getProgram().getFunctionManager().getFunctionContaining(ins.getMinAddress()); + if (function == null) { + return null; + } + Variable variable = VariableValueUtils.findStackVariable(function, stackAddress); + return executeBackground(monitor -> { + UnwoundFrame frame = eval.getStackFrame(function, warnings, monitor); + if (frame == null) { + throw new UnwindException("Cannot find frame for " + function); + } + if (variable != null) { + return fillFrameStorage(frame, variable.getName(), variable.getDataType(), + variable.getVariableStorage()); + } + Address dynAddr = frame.getBasePointer().add(stackAddress.getOffset()); + TraceCodeUnit unit = current.getTrace() + .getCodeManager() + .codeUnits() + .getContaining(current.getViewSnap(), dynAddr); + if (unit instanceof TraceData data && ListingUnwoundFrame.isFrame(data)) { + int offset = (int) dynAddr.subtract(data.getMinAddress()); + TraceData comp = data.getComponentContaining(offset); + return fillCodeUnit(comp, null, null); + } + return fillCodeUnit(unit, null, null); + }); + } + + public CompletableFuture fillReference(CodeUnit unit, + Address refAddress) { + if (refAddress.isMemoryAddress()) { + return fillMemory(unit.getProgram(), refAddress); + } + if (refAddress.isStackAddress() && unit instanceof Instruction ins) { + return fillStack(ins, refAddress); + } + return null; + } + + public VariableValueTable fillRegisterNoFrame(Register register) { + TraceData data = eval.getRegisterUnit(register); + if (data != null) { + table.add(new NameRow(register.getName())); + table.add(new TypeRow(data.getDataType())); + IntegerRow intRow = IntegerRow.fromCodeUnit(data, current.getSnap()); + table.add(intRow); + table.add(new ValueRow(data.getDefaultValueRepresentation(), intRow.state())); + return table; + } + // Just display the raw register value + table.add(new NameRow(register.getName())); + WatchValue raw = eval.getRawRegisterValue(register); + table.add(new IntegerRow(raw)); + return table; + } + + public CompletableFuture fillRegister(Instruction ins, + Register register) { + Function function = + ins.getProgram().getFunctionManager().getFunctionContaining(ins.getMinAddress()); + Variable variable = + function == null ? null : VariableValueUtils.findVariable(function, register); + return executeBackground(monitor -> { + UnwoundFrame frame; + if (function == null) { + warnings.add("Instruction is not in a function. Using innermost frame."); + frame = VariableValueUtils.locateInnermost(tool, current); + } + else { + frame = eval.getStackFrame(function, warnings, monitor); + } + if (frame == null) { + warnings.add( + "Could not locate " + function + " in stack. Using innermost frame."); + return fillRegisterNoFrame(register); + } + + if (variable != null) { + return fillFrameStorage(frame, variable.getName(), variable.getDataType(), + variable.getVariableStorage()); + } + + if (frame.getLevel() == 0) { + return fillRegisterNoFrame(register); + } + + // Still raw register value, but this time it can be restored from stack + table.add(new NameRow(register.getName())); + if (!frame.isFake()) { + table.add(new FrameRow(frame)); + } + WatchValue value = frame.getValue(register); + table.add(LocationRow.fromWatchValue(value, current.getPlatform().getLanguage())); + table.add(new IntegerRow(value)); + return table; + }); + } + + public CompletableFuture fillOperand(OperandFieldLocation opLoc, + Instruction ins) { + RefType refType = ins.getOperandRefType(opLoc.getOperandIndex()); + if (refType.isFlow()) { + return null; + } + Object operand = ins.getDefaultOperandRepresentationList(opLoc.getOperandIndex()) + .get(opLoc.getSubOperandIndex()); + if (operand instanceof Register register) { + return fillRegister(ins, register); + } + Address refAddress = opLoc.getRefAddress(); + if (operand instanceof Scalar scalar && refAddress != null) { + return fillReference(ins, refAddress); + } + if (operand instanceof Address address) { + return fillReference(ins, address); + } + return null; + } + + public CompletableFuture fillStorage(Function function, String name, + DataType type, VariableStorage storage, AddressSetView symbolStorage) { + return executeBackground(monitor -> { + UnwoundFrame frame = + VariableValueUtils.requiresFrame(storage, symbolStorage) + ? eval.getStackFrame(function, warnings, monitor) + : eval.getGlobalsFakeFrame(); + if (frame == null) { + throw new UnwindException("Cannot find frame for " + function); + } + return fillFrameStorage(frame, name, type, storage); + }); + } + + public CompletableFuture fillPcodeOp(Function function, String name, + DataType type, PcodeOp op, AddressSetView symbolStorage) { + return executeBackground(monitor -> { + UnwoundFrame frame = VariableValueUtils.requiresFrame(op, symbolStorage) + ? eval.getStackFrame(function, warnings, monitor) + : eval.getGlobalsFakeFrame(); + if (frame == null) { + throw new UnwindException("Cannot find frame for " + function); + } + return fillFrameOp(frame, function.getProgram(), name, type, op, symbolStorage); + }); + } + + public VariableValueTable fillWatchValue(UnwoundFrame frame, Address address, + DataType type, WatchValue value) { + table.add(LocationRow.fromWatchValue(value, current.getPlatform().getLanguage())); + if (value.address() != null && !value.address().isRegisterAddress()) { + table.add(new BytesRow(value)); + } + table.add(new IntegerRow(value)); + if (type != DataType.DEFAULT) { + String repr = eval.getRepresentation(frame, address, value, type); + table.add(new ValueRow(repr, value.state())); + } + return table; + } + + public VariableValueTable fillFrameStorage(UnwoundFrame frame, String name, + DataType type, VariableStorage storage) { + table.add(new NameRow(name)); + if (!frame.isFake()) { + table.add(new FrameRow(frame)); + } + table.add(new StorageRow(storage)); + table.add(new TypeRow(type)); + WatchValue value = frame.getValue(storage); + return fillWatchValue(frame, storage.getMinAddress(), type, value); + } + + public VariableValueTable fillFrameOp(UnwoundFrame frame, Program program, + String name, DataType type, PcodeOp op, AddressSetView symbolStorage) { + table.add(new NameRow(name)); + if (!frame.isFake()) { + table.add(new FrameRow(frame)); + } + table.add(new TypeRow(type)); + WatchValue value = frame.evaluate(program, op, symbolStorage); + // TODO: What if the type is dynamic with non-fixed size? + if (type.getLength() != value.length()) { + value = frame.zext(value, type.getLength()); + } + return fillWatchValue(frame, op.getOutput().getAddress(), type, value); + } + + public CompletableFuture fillHighVariable(HighVariable hVar, + String name, + AddressSetView symbolStorage) { + Function function = hVar.getHighFunction().getFunction(); + VariableStorage storage = VariableValueUtils.fabricateStorage(hVar); + if (storage.isUniqueStorage()) { + table.add(new NameRow(name)); + table.add(new StorageRow(storage)); + table.add(new ValueRow("(Unique)", TraceMemoryState.KNOWN)); + return CompletableFuture.completedFuture(table); + } + return fillStorage(function, name, hVar.getDataType(), storage, symbolStorage); + } + + public CompletableFuture fillHighVariable(HighVariable hVar, + AddressSetView symbolStorage) { + return fillHighVariable(hVar, hVar.getName(), symbolStorage); + } + + public CompletableFuture fillComponent(ClangFieldToken token, + AddressSetView symbolStorage) { + Function function = token.getClangFunction().getHighFunction().getFunction(); + Program program = function.getProgram(); + PcodeOp op = token.getPcodeOp(); + Varnode vn = op.getOutput(); + HighVariable hVar = vn.getHigh(); + DataType type = DataTypeDecompilerHover.getFieldDataType(token); + if (hVar.getDataType().isEquivalent(new PointerDataType(type))) { + op = VariableValueUtils.findDeref(program.getAddressFactory(), vn); + } + return fillPcodeOp(function, token.getText(), type, op, symbolStorage); + } + + public CompletableFuture fillComposite(HighSymbol hSym, + HighVariable hVar, AddressSetView symbolStorage) { + return fillStorage(hVar.getHighFunction().getFunction(), hSym.getName(), + hSym.getDataType(), hSym.getStorage(), symbolStorage); + } + + public CompletableFuture fillToken(ClangToken token) { + if (token == null) { + return null; + } + + /** + * I can't get just the expression tree here, except as p-code AST, which doesn't seem + * to include token info. A line should contain the full expression, though. I'll grab + * the symbols' storage from it and ensure my evaluation recurses until it hits those + * symbols. + */ + AddressSet symbolStorage = + VariableValueUtils.collectSymbolStorage(token.getLineParent()); + + if (token instanceof ClangFieldToken fieldToken) { + return fillComponent(fieldToken, symbolStorage); + } + + HighVariable hVar = token.getHighVariable(); + if (hVar == null) { + return null; + } + + HighSymbol hSym = hVar.getSymbol(); + if (hSym == null) { + // This is apparently the case for literals. + return null; + } + VariableStorage storage = hSym.getStorage(); + + String name = hVar.getName(); + if (name == null) { + name = hSym.getName(); + } + + Varnode representative = hVar.getRepresentative(); + if (!storage.contains(representative.getAddress())) { + // I'm not sure this can ever happen.... + return fillHighVariable(hVar, symbolStorage); + } + + if (Arrays.asList(storage.getVarnodes()).equals(List.of(representative))) { + // The var is the symbol + return fillHighVariable(hVar, symbolStorage); + } + + // Presumably, there's some component path from symbol to high var + return fillComposite(hSym, hVar, symbolStorage); + } + + public CompletableFuture fillVariable(Variable variable) { + Function function = variable.getFunction(); + return executeBackground(monitor -> { + UnwoundFrame frame = eval.getStackFrame(function, warnings, monitor); + if (frame == null) { + throw new UnwindException("Cannot find frame for " + function); + } + return fillFrameStorage(frame, variable.getName(), variable.getDataType(), + variable.getVariableStorage()); + }); + } + } + + public CompletableFuture fillVariableValueTable(VariableValueTable table, + ProgramLocation programLocation, DebuggerCoordinates current, + FieldLocation fieldLocation, Field field, List warnings) { + if (traceManager == null || mappingService == null) { + return null; + } + VariableEvaluator eval; + synchronized (cachedEvaluators) { + eval = cachedEvaluators.computeIfAbsent(current, c -> new VariableEvaluator(tool, c)); + } + TableFiller filler = new TableFiller(table, tool, current, eval, warnings); + if (field instanceof ClangTextField clangField) { + return filler.fillToken(clangField.getToken(fieldLocation)); + } + if (programLocation == null) { + return null; + } + Address refAddress = programLocation.getRefAddress(); + CodeUnit unit = programLocation.getProgram() + .getListing() + .getCodeUnitContaining(programLocation.getAddress()); + if (programLocation instanceof OperandFieldLocation opLoc && + unit instanceof Instruction ins) { + return filler.fillOperand(opLoc, ins); + } + if (programLocation instanceof OperandFieldLocation && refAddress != null && + refAddress.isMemoryAddress()) { + return filler.fillReference(unit, refAddress); + } + if (programLocation instanceof VariableLocation varLoc) { + return filler.fillVariable(varLoc.getVariable()); + } + return null; + } + + @Override + public JComponent getHoverComponent(Program program, ProgramLocation programLocation, + FieldLocation fieldLocation, Field field) { + if (!enabled || traceManager == null) { + return null; + } + VariableValueTable table = new VariableValueTable(); + List warnings = new ArrayList<>(); + CompletableFuture future; + try { + future = fillVariableValueTable(table, programLocation, + traceManager.getCurrent(), fieldLocation, field, warnings); + } + catch (Exception e) { + table.add(new ErrorRow(e)); + return createTooltipComponent("" + table.toHtml()); + } + if (future == null) { + return null; + } + if (!future.isDone()) { + table.add(new StatusRow("In Progress")); + } + JComponent component = createTooltipComponent("" + table.toHtml()); + if (!(component instanceof JToolTip tooltip)) { + throw new AssertionError("Expected a JToolTip"); + } + future.handleAsync((__, ex) -> { + table.remove(RowKey.STATUS); + if (ex != null) { + table.add(new ErrorRow(AsyncUtils.unwrapThrowable(ex))); + } + else { + table.add(new WarningsRow(warnings)); + } + tooltip.setTipText("" + table.toHtml()); + Window window = SwingUtilities.getWindowAncestor(tooltip); + if (window != null) { + window.pack(); + } // else, the computation completed before tooltip was returned + return null; + }, SwingExecutorService.MAYBE_NOW); + return tooltip; + } + + public void traceClosed(Trace trace) { + synchronized (cachedEvaluators) { + cachedEvaluators.keySet().removeIf(coords -> coords.getTrace() == trace); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueRow.java new file mode 100644 index 0000000000..a1b1445df2 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueRow.java @@ -0,0 +1,647 @@ +/* ### + * 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.stack.vars; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import generic.theme.GColor; +import ghidra.app.plugin.core.debug.stack.UnwoundFrame; +import ghidra.pcode.exec.DebuggerPcodeUtils.PrettyBytes; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValue; +import ghidra.pcode.exec.ValueLocation; +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.data.DataType; +import ghidra.program.model.lang.Language; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.MemoryAccessException; +import ghidra.program.model.pcode.Varnode; +import ghidra.trace.model.Trace; +import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.listing.TraceCodeUnit; +import ghidra.trace.model.memory.*; +import ghidra.trace.util.TraceAddressSpace; +import ghidra.util.HTMLUtilities; +import ghidra.util.exception.InvalidInputException; + +/** + * A row to be displayed in a variable value hover's table + */ +public interface VariableValueRow { + // TODO: Colors specific to hovers? + GColor COLOR_ERROR = new GColor("color.fg.error"); + GColor COLOR_STALE = new GColor("color.fg.debugger.value.stale"); + + /** + * Perform the simplest styling of the object + * + *

+ * This merely invokes the object's {@link Object#toString()} method and escapes its. If it's + * null, it will render "None" is the error color. + * + * @param obj the object, possibly null + * @return the HTML-styled string + */ + static String styleSimple(Object obj) { + return obj == null ? htmlFg(COLOR_ERROR, "None") : HTMLUtilities.escapeHTML(obj.toString()); + } + + /** + * Style a given string according to the given memory state + * + *

+ * This renders stale ({@link TraceMemoryState#UNKNOWN}) values in the stale color, usually + * gray. + * + * @param state the state + * @param str the HTML string + * @return the HTML-styled string + */ + static String styleState(TraceMemoryState state, String str) { + if (state == TraceMemoryState.KNOWN) { + return str; + } + return "" + str + ""; + } + + /** + * Escape and style the given text in the given color + * + * @param color the color + * @param text the text + * @return the HTML-styled string + */ + static String htmlFg(GColor color, String text) { + return "" + HTMLUtilities.escapeHTML(text) + + ""; + } + + /** + * A key naming a given row type + * + *

+ * This ensures the rows always appear in conventional order, and that there is only one of + * each. + */ + enum RowKey { + NAME("Name"), + FRAME("Frame"), + STORAGE("Storage"), + TYPE("Type"), + INSTRUCTION("Instruction"), + LOCATION("Location"), + BYTES("Bytes"), + INTEGER("Integer"), + VALUE("Value"), + STATUS("Status"), + WARNINGS("Warnings"), + ERROR("Error"), + ; + + private final String display; + + RowKey(String display) { + this.display = display; + } + + @Override + public String toString() { + return display; + } + } + + /** + * Get the key for this row type + * + * @return the key + */ + RowKey key(); + + /** + * Render the key for display in diagnostics + * + * @return the the key as a string + */ + default String keyToSimpleString() { + return key().toString(); + } + + /** + * Render the key for display in the table + * + * @return the key as an HTML string + */ + default String keyToHtml() { + return HTMLUtilities.escapeHTML(key() + ":"); + } + + /** + * Render the value for display in diagnostics + * + * @return the value as a string + */ + String valueToSimpleString(); + + /** + * Render the value for display in the table + * + * @return the value as an HTML string + */ + String valueToHtml(); + + /** + * Render this complete row for display in diagnostics + * + * @return the row as a string + */ + default String toSimpleString() { + return String.format("%s: %s", keyToSimpleString(), valueToSimpleString()); + } + + /** + * Render this complete row for display in the table + * + * @return the row as an HTMl string + */ + default String toHtml() { + return String.format("%s%s", + keyToHtml(), valueToHtml()); + } + + /** + * A row for the variable's name + */ + record NameRow(String name) implements VariableValueRow { + @Override + public RowKey key() { + return RowKey.NAME; + } + + @Override + public String valueToHtml() { + return styleSimple(name); + } + + @Override + public String valueToSimpleString() { + return name; + } + } + + /** + * A row for the frame used to compute the location and value + */ + record FrameRow(UnwoundFrame frame) implements VariableValueRow { + @Override + public RowKey key() { + return RowKey.FRAME; + } + + @Override + public String valueToHtml() { + return styleSimple(frame.getDescription()); + } + + @Override + public String valueToSimpleString() { + return frame.getDescription(); + } + } + + /** + * A row for the variable's statically-defined storage + */ + record StorageRow(VariableStorage storage) implements VariableValueRow { + public static StorageRow fromCodeUnit(CodeUnit unit) { + try { + return new StorageRow(new VariableStorage(unit.getProgram(), + new Varnode(unit.getMinAddress(), unit.getLength()))); + } + catch (InvalidInputException e) { + throw new AssertionError(e); + } + } + + @Override + public RowKey key() { + return RowKey.STORAGE; + } + + @Override + public String valueToHtml() { + return styleSimple(storage); + } + + @Override + public String valueToSimpleString() { + return storage.toString(); + } + } + + /** + * A row for the variable's type + */ + record TypeRow(DataType type) implements VariableValueRow { + @Override + public RowKey key() { + return RowKey.TYPE; + } + + @Override + public String valueToHtml() { + return styleSimple(type.getDisplayName()); + } + + @Override + public String valueToSimpleString() { + return type.getDisplayName(); + } + } + + /** + * If an operand refers to code, a row for the target instruction + */ + record InstructionRow(Instruction instruction) implements VariableValueRow { + @Override + public RowKey key() { + return RowKey.INSTRUCTION; + } + + @Override + public String valueToHtml() { + return styleSimple(instruction); + } + + @Override + public String valueToSimpleString() { + return instruction.toString(); + } + } + + /** + * A row for the variable's dynamic location + */ + record LocationRow(String locString) implements VariableValueRow { + /** + * Create a row from the given range + * + * @param range the range + * @return the row + */ + public static LocationRow fromRange(AddressRange range) { + return new LocationRow( + String.format("%s:%d", range.getMinAddress(), range.getLength())); + } + + /** + * Create a row from the given code unit + * + * @param unit the unit + * @return the row + */ + public static LocationRow fromCodeUnit(CodeUnit unit) { + return new LocationRow( + String.format("%s:%d", unit.getMinAddress(), unit.getLength())); + } + + /*** + * Create a row from the given watch value + * + * @param value the value + * @param language the language (for register name substitution) + * @return the row + */ + public static LocationRow fromWatchValue(WatchValue value, Language language) { + ValueLocation loc = value.location(); + if (loc == null || loc.isEmpty()) { + return new LocationRow(null); + } + return new LocationRow(loc.toString(language)); + } + + @Override + public RowKey key() { + return RowKey.LOCATION; + } + + @Override + public String valueToHtml() { + return styleSimple(locString); + } + + @Override + public String valueToSimpleString() { + return locString == null ? "None" : locString; + } + } + + /** + * Compute the memory state of a given range + * + *

+ * If any part of the range is not {@link TraceMemoryState#KNOWN} the result is + * {@link TraceMemoryState#UNKNOWN}. + * + * @param trace the trace + * @param space the thread, frame level, and address space + * @param range the address range + * @param snap the snapshot key + * @return the composite state + */ + static TraceMemoryState computeState(Trace trace, TraceAddressSpace space, AddressRange range, + long snap) { + TraceMemoryManager mem = trace.getMemoryManager(); + TraceMemoryOperations ops; + if (space != null && space.getAddressSpace().isRegisterSpace()) { + ops = mem.getMemoryRegisterSpace(space.getThread(), space.getFrameLevel(), false); + } + else { + ops = mem; + } + return ops != null && ops.isKnown(snap, range) + ? TraceMemoryState.KNOWN + : TraceMemoryState.UNKNOWN; + } + + /** + * Compute the memory state of a given code unit + * + * @param unit the code unit + * @param snap the snapshot key + * @return the composite state. + * @see #computeState(Trace, TraceAddressSpace, AddressRange, long) + */ + static TraceMemoryState computeState(TraceCodeUnit unit, long snap) { + return computeState(unit.getTrace(), unit.getTraceSpace(), unit.getRange(), snap); + } + + /** + * A row to display the bytes in the variable + */ + record BytesRow(PrettyBytes bytes, TraceMemoryState state) implements VariableValueRow { + /** + * Create a row from a given range + * + * @param platform the platform (for trace memory and language) + * @param range the range + * @param snap the snapshot key + * @return the row + */ + public static BytesRow fromRange(TracePlatform platform, AddressRange range, long snap) { + long size = range.getLength(); + ByteBuffer buf = ByteBuffer.allocate((int) size); + Trace trace = platform.getTrace(); + if (size != trace.getMemoryManager().getViewBytes(snap, range.getMinAddress(), buf)) { + throw new AssertionError(new MemoryAccessException("Could not read bytes")); + } + return new BytesRow( + new PrettyBytes(platform.getLanguage().isBigEndian(), buf.array()), + computeState(trace, null, range, snap)); + } + + /** + * Create a row from a given code unit + * + * @param unit unit + * @param snap the snapshot key + * @return the row + */ + public static BytesRow fromCodeUnit(TraceCodeUnit unit, long snap) { + try { + return new BytesRow(new PrettyBytes(unit.isBigEndian(), unit.getBytes()), + computeState(unit, snap)); + } + catch (MemoryAccessException e) { + throw new AssertionError(e); + } + } + + /** + * Create a row from a given watch value + * + * @param value the value + */ + public BytesRow(WatchValue value) { + this(value.bytes(), value.state()); + } + + @Override + public RowKey key() { + return RowKey.BYTES; + } + + @Override + public String valueToHtml() { + return styleState(state, bytes.toBytesString().replace("\n", "
")); + } + + @Override + public String valueToSimpleString() { + return String.format("(%s) %s", state, bytes.toBytesString()); + } + } + + /** + * A row to display a variable's value as an integer in various formats + */ + record IntegerRow(PrettyBytes bytes, TraceMemoryState state) implements VariableValueRow { + /** + * Create a row from a given code unit + * + * @param unit the unit + * @param snap the snapshot key + * @return the row + */ + public static IntegerRow fromCodeUnit(TraceCodeUnit unit, long snap) { + try { + return new IntegerRow(new PrettyBytes(unit.isBigEndian(), unit.getBytes()), + computeState(unit, snap)); + } + catch (MemoryAccessException e) { + throw new AssertionError(e); + } + } + + /** + * Create a row from the given {@link BytesRow} + * + * @param bytes the bytes row + */ + public IntegerRow(BytesRow bytes) { + this(bytes.bytes, bytes.state); + } + + /** + * Create a row from the given watch value + * + * @param value the value + */ + public IntegerRow(WatchValue value) { + this(value.bytes(), value.state()); + } + + @Override + public RowKey key() { + return RowKey.INTEGER; + } + + @Override + public String toHtml() { + if (bytes.length() > 16) { + return ""; + } + return VariableValueRow.super.toHtml(); + } + + @Override + public String valueToHtml() { + return styleState(state, bytes.collectDisplays().replace("\n", "
")); + } + + @Override + public String valueToSimpleString() { + return String.format("(%s) %s", state, bytes.collectDisplays()); + } + } + + /** + * A row to display the variable's value in its type's default representation + */ + record ValueRow(String value, TraceMemoryState state) implements VariableValueRow { + @Override + public RowKey key() { + return RowKey.VALUE; + } + + @Override + public String valueToHtml() { + return styleState(state, HTMLUtilities.escapeHTML(value)); + } + + @Override + public String valueToSimpleString() { + return String.format("(%s) %s", state, value); + } + } + + /** + * A row to indicate the computation status, in case it takes a moment + */ + record StatusRow(String status) implements VariableValueRow { + @Override + public RowKey key() { + return RowKey.STATUS; + } + + @Override + public String valueToHtml() { + return String.format("%s", HTMLUtilities.escapeHTML(status.toString())); + } + + @Override + public String valueToSimpleString() { + return status.toString(); + } + } + + /** + * A row to display the warnings encountered while unwinding the frame used to evaluate the + * variable + */ + record WarningsRow(String warnings) implements VariableValueRow { + /** + * Create a row from the given list of warnings + * + * @param warnings the warnings + */ + public WarningsRow(List warnings) { + this(warnings.stream() + .map(String::trim) + .filter(w -> !w.isBlank()) + .collect(Collectors.joining("\n"))); + } + + @Override + public RowKey key() { + return RowKey.WARNINGS; + } + + @Override + public String keyToHtml() { + return htmlFg(COLOR_ERROR, key() + ":"); + } + + @Override + public String valueToHtml() { + String[] split = warnings.split("\n"); + String formatted = Stream.of(split) + .map(w -> String.format("

  • %s
  • ", HTMLUtilities.escapeHTML(w))) + .collect(Collectors.joining("\n ")); + return String.format(""" +
      + %s +
    + """, formatted); + } + + @Override + public String valueToSimpleString() { + return warnings; + } + + @Override + public String toHtml() { + if (warnings.isBlank()) { + return ""; + } + return String.format("%s%s", + keyToHtml(), valueToHtml()); + } + } + + /** + * A row to display an error in case the table is incomplete + */ + record ErrorRow(Throwable error) implements VariableValueRow { + @Override + public RowKey key() { + return RowKey.ERROR; + } + + @Override + public String keyToHtml() { + return htmlFg(COLOR_ERROR, key() + ":"); + } + + @Override + public String valueToHtml() { + return styleSimple(error); + } + + @Override + public String valueToSimpleString() { + return error.toString(); + } + + @Override + public String toHtml() { + return String.format("%s%s", + keyToHtml(), valueToHtml()); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueTable.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueTable.java new file mode 100644 index 0000000000..d982127d62 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueTable.java @@ -0,0 +1,115 @@ +/* ### + * 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.stack.vars; + +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import ghidra.app.plugin.core.debug.gui.stack.vars.VariableValueRow.RowKey; + +/** + * A table for display in a variable value hover + */ +public class VariableValueTable { + private final Map rows = new TreeMap<>(); + + /** + * Add a row to the table + *

    + * At most one of each row type can be present. Adding a row whose type already exists will + * remove the old row of the same type. + * + * @param row + */ + public void add(VariableValueRow row) { + synchronized (rows) { + rows.put(row.key(), row); + } + } + + @Override + public String toString() { + synchronized (rows) { + return String.format(""" + <%s: + %s + > + """, + getClass().getSimpleName(), + rows.values() + .stream() + .map(VariableValueRow::toSimpleString) + .collect(Collectors.joining("\n "))); + } + } + + /** + * Render the table as HTML for display in the GUI + * + *

    + * The rows are always ordered as in {@link RowKey}. + * + * @return the HTML string + */ + public String toHtml() { + synchronized (rows) { + return String.format(""" + + %s +
    + """, + rows.values() + .stream() + .map(VariableValueRow::toHtml) + .collect(Collectors.joining("\n"))); + } + } + + /** + * Count the number of rows + * + * @return the count + */ + public int getNumRows() { + synchronized (rows) { + return rows.size(); + } + } + + /** + * Get the row of the given type + * + * @param key the key / type + * @return the row, or null + */ + public VariableValueRow get(RowKey key) { + synchronized (rows) { + return rows.get(key); + } + } + + /** + * Remove the row of the given type + * + * @param key the key / type + */ + public void remove(RowKey key) { + synchronized (rows) { + rows.remove(key); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueUtils.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueUtils.java new file mode 100644 index 0000000000..b8990b686a --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueUtils.java @@ -0,0 +1,847 @@ +/* ### + * 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.stack.vars; + +import java.util.*; + +import ghidra.app.decompiler.ClangLine; +import ghidra.app.decompiler.ClangToken; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.stack.*; +import ghidra.docking.settings.Settings; +import ghidra.docking.settings.SettingsDefinition; +import ghidra.framework.plugintool.PluginTool; +import ghidra.pcode.eval.AbstractVarnodeEvaluator; +import ghidra.pcode.exec.DebuggerPcodeUtils; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValue; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValuePcodeExecutorState; +import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; +import ghidra.pcode.opbehavior.BinaryOpBehavior; +import ghidra.pcode.opbehavior.UnaryOpBehavior; +import ghidra.program.model.address.*; +import ghidra.program.model.data.*; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.ByteMemBufferImpl; +import ghidra.program.model.mem.Memory; +import ghidra.program.model.pcode.*; +import ghidra.trace.model.*; +import ghidra.trace.model.Trace.TraceMemoryBytesChangeType; +import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.listing.*; +import ghidra.trace.model.memory.*; +import ghidra.trace.model.stack.TraceStack; +import ghidra.trace.model.stack.TraceStackFrame; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.util.TraceAddressSpace; +import ghidra.util.MathUtilities; +import ghidra.util.Msg; +import ghidra.util.exception.InvalidInputException; +import ghidra.util.task.TaskMonitor; + +/** + * Various utilities for evaluating statically-defined variables in the context of a dynamic trace. + */ +public enum VariableValueUtils { + ; + + /** + * An "evaluator" which simply determines whether actual evaluation will require a frame for + * context + */ + private static final class RequiresFrameEvaluator extends AbstractVarnodeEvaluator { + private final AddressSetView symbolStorage; + + private RequiresFrameEvaluator(AddressSetView symbolStorage) { + this.symbolStorage = symbolStorage; + } + + @Override + protected boolean isLeaf(Varnode vn) { + return vn.isConstant() || + symbolStorage.contains(vn.getAddress(), vn.getAddress().add(vn.getSize() - 1)); + } + + @Override + protected Address applyBase(long offset) { + throw new AssertionError(); + } + + @Override + protected Boolean evaluateConstant(long value, int size) { + return false; + } + + @Override + protected Boolean evaluateRegister(Address address, int size) { + return true; + } + + @Override + protected Boolean evaluateStack(long offset, int size) { + return true; + } + + @Override + protected Boolean evaluateMemory(Address address, int size) { + return false; + } + + @Override + protected Boolean evaluateUnique(long offset, int size) { + /** + * Generally speaking, this getting called is bad. We'll "let it go" here and the error + * should surface in actual evaluation. + */ + return false; + } + + @Override + protected Boolean evaluateAbstract(Program program, AddressSpace space, Boolean offset, + int size, Map already) { + /** + * This generally happens for dereferences. The evaluator will already have determined + * if computing the address requires a frame, which we should just echo back. Neither + * the location of the target nor its value has any bearing on whether or not a frame is + * required. + */ + return offset; + } + + @Override + protected Boolean evaluateUnaryOp(Program program, PcodeOp op, UnaryOpBehavior unOp, + Map already) { + return evaluateVarnode(program, op.getInput(0), already); + } + + @Override + protected Boolean evaluateBinaryOp(Program program, PcodeOp op, BinaryOpBehavior binOp, + Map already) { + return evaluateVarnode(program, op.getInput(0), already) || + evaluateVarnode(program, op.getInput(1), already); + } + + @Override + protected Boolean evaluateLoad(Program program, PcodeOp op, + Map already) { + return evaluateVarnode(program, op.getInput(1), already); + } + + @Override + protected Boolean evaluatePtrAdd(Program program, PcodeOp op, + Map already) { + // Third input is a constant, according to pcoderef.xml + return evaluateBinaryOp(program, op, null, already); + } + + @Override + protected Boolean evaluatePtrSub(Program program, PcodeOp op, + Map already) { + return evaluateBinaryOp(program, op, null, already); + } + + @Override + protected Boolean catenate(int total, Boolean value, Boolean piece, int size) { + return value || piece; + } + + @Override + public Boolean evaluateStorage(VariableStorage storage) { + return evaluateStorage(storage, false); + } + } + + /** + * A settings that provides the given space as the default for pointers + */ + static class DefaultSpaceSettings implements Settings { + final Settings delegate; + final AddressSpace space; + + public DefaultSpaceSettings(Settings delegate, AddressSpace space) { + this.delegate = delegate; + this.space = space; + } + + @Override + public boolean isChangeAllowed(SettingsDefinition settingsDefinition) { + return delegate.isChangeAllowed(settingsDefinition); + } + + @Override + public Long getLong(String name) { + return delegate.getLong(name); + } + + @Override + public String getString(String name) { + if (AddressSpaceSettingsDefinition.DEF.getStorageKey().equals(name)) { + return space.getName(); + } + return delegate.getString(name); + } + + @Override + public Object getValue(String name) { + if (AddressSpaceSettingsDefinition.DEF.getStorageKey().equals(name)) { + return space.getName(); + } + return delegate.getValue(name); + } + + @Override + public void setLong(String name, long value) { + throw new UnsupportedOperationException(); + } + + @Override + public void setString(String name, String value) { + throw new UnsupportedOperationException(); + } + + @Override + public void setValue(String name, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public void clearSetting(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public void clearAllSettings() { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getNames() { + return delegate.getNames(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public Settings getDefaultSettings() { + return delegate.getDefaultSettings(); + } + } + + /** + * Compute the address range where annotated frames would be expected in the listing + * + * @param coordinates the coordinates + * @return the range, usually from stack pointer to the end of the stack segment + */ + public static AddressRange computeFrameSearchRange(DebuggerCoordinates coordinates) { + TraceThread thread = coordinates.getThread(); + if (thread == null) { + return null; + } + Trace trace = thread.getTrace(); + long viewSnap = coordinates.getViewSnap(); + TraceMemoryManager mem = trace.getMemoryManager(); + TracePlatform platform = coordinates.getPlatform(); + CompilerSpec cSpec = platform.getCompilerSpec(); + Register sp = cSpec.getStackPointer(); + + TraceMemorySpace regs = mem.getMemoryRegisterSpace(thread, 0, false); + RegisterValue spRV = regs.getValue(platform, viewSnap, sp); + Address spVal = cSpec.getStackBaseSpace().getAddress(spRV.getUnsignedValue().longValue()); + Address max; + TraceMemoryRegion stackRegion = mem.getRegionContaining(coordinates.getSnap(), spVal); + if (stackRegion != null) { + max = stackRegion.getMaxAddress(); + } + else { + long toMax = spVal.getAddressSpace().getMaxAddress().subtract(spVal); + max = spVal.add(MathUtilities.unsignedMin(4095, toMax)); + } + return new AddressRangeImpl(spVal, max); + } + + /** + * Find the innermost frame for the given coordinates + * + * @param tool the tool + * @param coordinates the coordinates + * @return the frame, or null + */ + public static ListingUnwoundFrame locateInnermost(PluginTool tool, + DebuggerCoordinates coordinates) { + AddressRange range = computeFrameSearchRange(coordinates); + if (range == null) { + return null; + } + // TODO: Positive stack growth? + for (TraceData data : coordinates.getTrace() + .getCodeManager() + .definedData() + .get(coordinates.getViewSnap(), range, true)) { + try { + return new ListingUnwoundFrame(tool, coordinates, data); + } + catch (UnwindException e) { + Msg.warn(VariableValueUtils.class, "Skipping frame " + data + ". " + e); + // Just try the next + } + } + return null; + } + + /** + * Locate an already unwound frame in the listing at the given coordinates + * + * @param tool the tool for context, especially for mappings to static programs + * @param coordinates the coordinates to search. Note that recursive calls are distinguished by + * the coordinates' frame level, though unwinding starts at frame 0. + * @param function the function the allocated the desired frame / call record + * @see AnalysisUnwoundFrame#applyToListing(int, TaskMonitor) + * @return the frame or null + */ + public static ListingUnwoundFrame locateFrame(PluginTool tool, DebuggerCoordinates coordinates, + Function function) { + int minLevel = coordinates.getFrame(); + AddressRange range = computeFrameSearchRange(coordinates); + if (range == null) { + return null; + } + + // TODO: Positive stack growth? + for (TraceData data : coordinates.getTrace() + .getCodeManager() + .definedData() + .get(coordinates.getViewSnap(), range, true)) { + try { + ListingUnwoundFrame frame = new ListingUnwoundFrame(tool, coordinates, data); + minLevel--; + if (minLevel < 0 && frame.getFunction() == function) { + return frame; + } + } + catch (UnwindException e) { + Msg.warn(VariableValueUtils.class, "Skipping frame " + data + ". " + e); + // Just try the next + } + } + Msg.info(VariableValueUtils.class, "Cannot find frame for function " + function); + return null; + } + + /** + * Check if evaluation of the given storage will require a frame + * + * @param storage the storage to evaluate + * @param symbolStorage the leaves of evaluation, usually storage used by symbols in scope. See + * {@link #collectSymbolStorage(ClangLine)} + * @return true if a frame is required, false otherwise + */ + public static boolean requiresFrame(VariableStorage storage, AddressSetView symbolStorage) { + return new RequiresFrameEvaluator(symbolStorage).evaluateStorage(storage); + } + + /** + * Check if evaluation of the given p-code op will require a frame + * + * @param op the op whose output to evaluation + * @param symbolStorage the leaves of evaluation, usually storage used by symbols in scope. See + * {@link #collectSymbolStorage(ClangLine)} + * @return true if a frame is required, false otherwise + */ + public static boolean requiresFrame(PcodeOp op, AddressSetView symbolStorage) { + return new RequiresFrameEvaluator(symbolStorage).evaluateOp(null, op); + } + + /** + * Get the program counter for the given thread's innermost frame using its {@link TraceStack} + * + *

    + * This will prefer the program counter in the {@link TraceStackFrame}. If that's not available, + * it will use the value of the program counter register from the thread's register bank for + * frame 0. + * + * @param platform the platform + * @param thread the thread + * @param snap the snapshot key + * @return the address + */ + public static Address getProgramCounterFromStack(TracePlatform platform, TraceThread thread, + long snap) { + TraceStack stack = thread.getTrace().getStackManager().getStack(thread, snap, false); + if (stack == null) { + return null; + } + TraceStackFrame frame = stack.getFrame(0, false); + if (frame == null) { + return null; + } + return frame.getProgramCounter(snap); + } + + /** + * Get the program counter for the given thread's innermost frame using its + * {@link TraceMemorySpace}, i.e., registers + * + * @param platform the platform + * @param thread the thread + * @param snap the snapshot key + * @return the address + */ + public static Address getProgramCounterFromRegisters(TracePlatform platform, TraceThread thread, + long snap) { + TraceMemorySpace regs = + thread.getTrace().getMemoryManager().getMemoryRegisterSpace(thread, false); + if (regs == null) { + return null; + } + RegisterValue value = + regs.getValue(platform, snap, platform.getLanguage().getProgramCounter()); + return platform.getLanguage() + .getDefaultSpace() + .getAddress(value.getUnsignedValue().longValue()); + } + + /** + * Get the program counter from the innermost frame of the given thread's stack + * + *

    + * This will prefer the program counter in the {@link TraceStackFrame}. If that's not available, + * it will use the value of the program counter register from the thread's register bank for + * frame 0. + * + * @param platform the platform + * @param thread the thread + * @param snap the snapshot key + * @return the address + */ + public static Address getProgramCounter(TracePlatform platform, TraceThread thread, long snap) { + Address pcFromStack = getProgramCounterFromStack(platform, thread, snap); + if (pcFromStack != null) { + return pcFromStack; + } + return getProgramCounterFromRegisters(platform, thread, snap); + } + + /** + * Check if the unwound frames annotated in the listing are "fresh" + * + *

    + * It can be difficult to tell. The heuristic we use is if the PC of the innermost frame agrees + * with the PC recorded for the current thread. + * + * @param tool the tool + * @param coordinates the coordinates + * @return true if the unwind appears fresh + */ + public static boolean hasFreshUnwind(PluginTool tool, DebuggerCoordinates coordinates) { + ListingUnwoundFrame innermost = locateInnermost(tool, coordinates); + if (innermost == null || !Objects.equals(innermost.getProgramCounter(), + getProgramCounter(coordinates.getPlatform(), coordinates.getThread(), + coordinates.getViewSnap()))) { + return false; + } + return true; + } + + /** + * Find the function's variable whose storage is exactly the given register + * + * @param function the function + * @param register the register + * @return the variable, or null + */ + public static Variable findVariable(Function function, Register register) { + for (Variable variable : function.getAllVariables()) { + if (variable.isRegisterVariable() && variable.getRegister() == register) { + return variable; + } + } + return null; + } + + /** + * Find the fuction's variable whose storage contains the given stack offset + * + * @param function the function + * @param stackAddress the stack offset + * @return the variable, or null + */ + public static Variable findStackVariable(Function function, Address stackAddress) { + if (!stackAddress.isStackAddress()) { + throw new IllegalArgumentException("stackAddress is not a stack address"); + } + return function.getStackFrame().getVariableContaining((int) stackAddress.getOffset()); + } + + /** + * Convert the given varnode to an address range + * + * @param vn the varnode + * @return the address range + */ + public static AddressRange rangeFromVarnode(Varnode vn) { + return new AddressRangeImpl(vn.getAddress(), vn.getAddress().add(vn.getSize() - 1)); + } + + /** + * Check if the given address set completely contains the given varnode + * + * @param set the set + * @param vn the varnode + * @return true if completely contained + */ + public static boolean containsVarnode(AddressSetView set, Varnode vn) { + return set.contains(vn.getAddress(), vn.getAddress().add(vn.getSize() - 1)); + } + + /** + * Collect the addresses used for storage by any symbol in the given line of decompiled C code + * + *

    + * It's not the greatest, but an variable to be evaluated should only be expressed in terms of + * symbols on the same line (at least by the decompiler's definition, wrapping shouldn't count + * against us). This can be used to determine where evaluation should cease descending into + * defining p-code ops. See {@link #requiresFrame(PcodeOp, AddressSetView)}, and + * {@link UnwoundFrame#evaluate(Program, PcodeOp, AddressSetView)}. + * + * @param line the line + * @return the address set + */ + public static AddressSet collectSymbolStorage(ClangLine line) { + AddressSet storage = new AddressSet(); + for (ClangToken tok : line.getAllTokens()) { + HighVariable hVar = tok.getHighVariable(); + if (hVar == null) { + continue; + } + HighSymbol hSym = hVar.getSymbol(); + if (hSym == null) { + continue; + } + for (Varnode vn : hSym.getStorage().getVarnodes()) { + storage.add(rangeFromVarnode(vn)); + } + } + return storage; + } + + /** + * Find the descendent that dereferences this given varnode + * + *

    + * This searches only one hop for a {@link PcodeOp#LOAD} or {@link PcodeOp#STORE}. If it find a + * load, it simply returns it. If it find a store, it generates the inverse load and returns it. + * This latter behavior ensures we can evaluate the lval or a decompiled assignment statement. + * + * @param factory an address factory for generating unique varnodes + * @param vn the varnode for which a dereference is expected + * @return the dereference, as a {@link PcodeOp#LOAD} + */ + public static PcodeOp findDeref(AddressFactory factory, Varnode vn) { + Iterable it = (Iterable) () -> vn.getDescendants(); + for (PcodeOp desc : it) { + if (desc.getOpcode() == PcodeOp.LOAD) { + return desc; + } + } + for (PcodeOp desc : it) { + if (desc.getOpcode() == PcodeOp.STORE) { + PcodeOpAST op = new PcodeOpAST(desc.getSeqnum(), PcodeOp.LOAD, 2); + op.setInput(desc.getInput(0), 0); + op.setInput(desc.getInput(1), 1); + VarnodeAST out = new VarnodeAST(factory.getUniqueSpace().getAddress(1L << 31), + desc.getInput(2).getSize(), 0xf00d); + op.setOutput(out); + out.setDef(op); + return op; + } + } + return null; + } + + /** + * Find an instance that occurs in the variable's symbol's storage + * + *

    + * This goal is to find a stable location for evaluating the high variable, rather than some + * temporary register or worse unique location. If no satisfying instance is found, it defaults + * to the variable's representative instance. + * + * @param hVar the high variable + * @return the instance found + */ + public static Varnode getInstanceInSymbolStorage(HighVariable hVar) { + Varnode representative = hVar.getRepresentative(); + HighSymbol hSym = hVar.getSymbol(); + if (hSym == null) { + return representative; + } + AddressSet storageSet = new AddressSet(); + for (Varnode vn : hSym.getStorage().getVarnodes()) { + storageSet.add(rangeFromVarnode(vn)); + } + if (containsVarnode(storageSet, representative)) { + return representative; + } + for (Varnode instance : hVar.getInstances()) { + if (containsVarnode(storageSet, instance)) { + return instance; + } + } + return representative; + } + + /** + * Create a {@link VariableStorage} object for the given high variable + * + *

    + * This is not necessarily the same as the variable's symbol's storage. In fact, if the variable + * represents a field, it is likely a subset of the symbol's storage. + * + * @param hVar the high variable + * @return the storage + */ + public static VariableStorage fabricateStorage(HighVariable hVar) { + try { + return new VariableStorage(hVar.getHighFunction().getFunction().getProgram(), + getInstanceInSymbolStorage(hVar)); + } + catch (InvalidInputException e) { + throw new AssertionError(e); + } + } + + /** + * A class which supports evaluating variables + */ + public static class VariableEvaluator { + /** + * A listener that invalidates the stack unwind whenever the trace's bytes change + */ + private class ListenerForChanges extends TraceDomainObjectListener { + public ListenerForChanges() { + listenFor(TraceMemoryBytesChangeType.CHANGED, this::bytesChanged); + } + + private void bytesChanged(TraceAddressSpace space, TraceAddressSnapRange range) { + TraceThread thread = space.getThread(); + // TODO: Consider the lifespan, too? Would have to use viewport.... + if (thread == null || thread == coordinates.getThread()) { + invalidateCache(); + } + } + } + + private final Object lock = new Object(); + private final PluginTool tool; + private final DebuggerCoordinates coordinates; + private final Language language; + private final ListenerForChanges listenerForChanges = new ListenerForChanges(); + + private List> unwound; + private FakeUnwoundFrame fakeFrame; + + /** + * Construct an evaluator for the given tool and coordinates + * + * @param tool the tool + * @param coordinates the coordinates + */ + public VariableEvaluator(PluginTool tool, DebuggerCoordinates coordinates) { + this.tool = tool; + this.coordinates = coordinates; + this.language = coordinates.getPlatform().getLanguage(); + + coordinates.getTrace().addListener(listenerForChanges); + } + + /** + * Dispose of this evaluator, removing its listener + */ + public void dispose() { + coordinates.getTrace().removeListener(listenerForChanges); + } + + /** + * Invalidate the stack unwind + */ + public void invalidateCache() { + synchronized (lock) { + unwound = null; + } + } + + /** + * Get a fake frame for global / static variables + * + * @return the fake frame + */ + public UnwoundFrame getGlobalsFakeFrame() { + synchronized (lock) { + if (fakeFrame == null) { + fakeFrame = new FakeUnwoundFrame<>(tool, coordinates, + DebuggerPcodeUtils.buildWatchState(tool, coordinates.frame(0))); + } + return fakeFrame; + } + } + + /** + * Refresh the stack unwind + * + * @param monitor a monitor for cancellation + */ + protected void doUnwind(TaskMonitor monitor) { + monitor.setMessage("Unwinding Stack"); + StackUnwinder unwinder = new StackUnwinder(tool, coordinates.getPlatform()); + unwound = new ArrayList<>(); + for (AnalysisUnwoundFrame frame : unwinder.frames(coordinates.frame(0), + monitor)) { + unwound.add(frame); + } + } + + /** + * Get the stack frame for the given function at or beyond the coordinates' frame level + * + * @param function the desired function + * @param warnings a place to emit warnings + * @param monitor a monitor for cancellation + * @return the frame if found, or null + */ + public UnwoundFrame getStackFrame(Function function, List warnings, + TaskMonitor monitor) { + synchronized (lock) { + if (unwound == null) { + doUnwind(monitor); + } + + for (UnwoundFrame frame : unwound.subList(coordinates.getFrame(), + unwound.size())) { + if (frame.getFunction() == function) { + String unwindWarnings = frame.getWarnings(); + if (unwindWarnings != null && !unwindWarnings.isBlank()) { + warnings.add(unwindWarnings); + } + return frame; + } + } + return null; + } + } + + /** + * Get the data unit for a register + * + *

    + * This accounts for memory-mapped registers. + * + * @param register the register + * @return the data unit, or null if undefined or mismatched + */ + public TraceData getRegisterUnit(Register register) { + TraceCodeOperations code; + TraceCodeManager codeManager = coordinates.getTrace().getCodeManager(); + if (register.getAddressSpace().isRegisterSpace()) { + TraceThread thread = coordinates.getThread(); + if (thread == null) { + return null; + } + code = codeManager.getCodeRegisterSpace(thread, false); + if (code == null) { + return null; + } + } + else { + code = codeManager; + } + return code.definedData() + .getForRegister(coordinates.getPlatform(), coordinates.getViewSnap(), register); + } + + /** + * Obtain the value of a register + * + *

    + * In order to accommodate user-provided types on registers, it's preferable to obtain the + * data unit using {@link #getRegisterUnit(Register)}. Fall back to this method only if that + * one fails. + * + * @param register + * @return + */ + public WatchValue getRawRegisterValue(Register register) { + WatchValuePcodeExecutorState state = + DebuggerPcodeUtils.buildWatchState(tool, coordinates.frame(0)); + return state.getVar(register, Reason.INSPECT); + } + + /** + * Get the representation of a variable's value according to a given data type + * + * @param address the best static address giving the location of the variable + * @param bytes the bytes giving the variable's value + * @param type the type of the variable + * @param settings settings to configure the data type + * @return the string representation, or null + */ + public String getRepresentation(Address address, byte[] bytes, DataType type, + Settings settings) { + if (type instanceof Pointer && !AddressSpaceSettingsDefinition.DEF.hasValue(settings) && + address.isRegisterAddress()) { + settings = new DefaultSpaceSettings(settings, language.getDefaultSpace()); + } + ByteMemBufferImpl buf = + new ByteMemBufferImpl(address, bytes, language.isBigEndian()) { + @Override + public Memory getMemory() { + return coordinates.getView().getMemory(); + } + }; + return type.getRepresentation(buf, settings, bytes.length); + } + + /** + * Get the representation of a variable's value according to a given data type + * + * @param frame the frame that evaluated the variable's value + * @param address the best static address giving the location of the variable. Note that the + * address given by {@link WatchValue#address()} is its dynamic address. The + * static address should instead be taken from the variable's storage or a p-code + * op's output varnode. + * @param value the value of the variable + * @param type the type of the variable + * @return the string representation, or null + */ + public String getRepresentation(UnwoundFrame frame, Address address, WatchValue value, + DataType type) { + if (type == DataType.DEFAULT) { + return null; + } + Settings settings = type.getDefaultSettings(); + if (address.isStackAddress()) { + address = frame.getBasePointer().add(address.getOffset()); + if (frame instanceof ListingUnwoundFrame listingFrame) { + settings = listingFrame.getComponentContaining(address); + } + } + return getRepresentation(address, value.bytes().bytes(), type, settings); + } + } +} 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 9fde899120..55214b963d 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 @@ -126,7 +126,7 @@ public class WatchRow { prevValue = prevExec == null ? null : compiled.evaluate(prevExec); TracePlatform platform = provider.current.getPlatform(); - value = fullValue.bytes(); + value = fullValue.bytes().bytes(); error = null; state = fullValue.state(); // TODO: Optional column for guest address? diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/AbstractRWTargetPcodeExecutorStatePiece.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/AbstractRWTargetPcodeExecutorStatePiece.java index ade16ff8a6..4aedcbd732 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/AbstractRWTargetPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/AbstractRWTargetPcodeExecutorStatePiece.java @@ -15,11 +15,14 @@ */ package ghidra.app.plugin.core.debug.service.emulation; +import java.util.Map; import java.util.concurrent.*; import ghidra.app.plugin.core.debug.service.emulation.data.PcodeDebuggerDataAccess; +import ghidra.generic.util.datastruct.SemisparseByteArray; import ghidra.pcode.exec.AccessPcodeExecutionException; import ghidra.pcode.exec.trace.BytesTracePcodeExecutorStatePiece; +import ghidra.pcode.exec.trace.data.PcodeTraceDataAccess; import ghidra.program.model.address.*; import ghidra.program.model.lang.Language; import ghidra.trace.model.memory.TraceMemoryState; @@ -44,6 +47,11 @@ public abstract class AbstractRWTargetPcodeExecutorStatePiece super(language, space, backing); } + protected AbstractRWTargetCachedSpace(Language language, AddressSpace space, + PcodeTraceDataAccess backing, SemisparseByteArray bytes, AddressSet written) { + super(language, space, backing, bytes, written); + } + protected abstract void fillUninitialized(AddressSet uninitialized); @Override @@ -99,6 +107,20 @@ public abstract class AbstractRWTargetPcodeExecutorStatePiece */ protected abstract class TargetBackedSpaceMap extends CacheingSpaceMap { + + public TargetBackedSpaceMap() { + super(); + } + + protected TargetBackedSpaceMap(Map spaces) { + super(spaces); + } + + @Override + public CachedSpace fork(CachedSpace s) { + return s.fork(); + } + @Override protected PcodeDebuggerDataAccess getBacking(AddressSpace space) { return data; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetMemoryPcodeExecutorState.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetMemoryPcodeExecutorState.java index 8bf601a6f0..85fb25c77b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetMemoryPcodeExecutorState.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetMemoryPcodeExecutorState.java @@ -16,7 +16,7 @@ package ghidra.app.plugin.core.debug.service.emulation; import ghidra.app.plugin.core.debug.service.emulation.data.PcodeDebuggerMemoryAccess; -import ghidra.pcode.exec.trace.DefaultTracePcodeExecutorState; +import ghidra.pcode.exec.trace.*; /** * A state composing a single {@link RWTargetMemoryPcodeExecutorStatePiece} @@ -31,4 +31,14 @@ public class RWTargetMemoryPcodeExecutorState extends DefaultTracePcodeExecutorS public RWTargetMemoryPcodeExecutorState(PcodeDebuggerMemoryAccess data, Mode mode) { super(new RWTargetMemoryPcodeExecutorStatePiece(data, mode)); } + + protected RWTargetMemoryPcodeExecutorState( + TracePcodeExecutorStatePiece piece) { + super(piece); + } + + @Override + public RWTargetMemoryPcodeExecutorState fork() { + return new RWTargetMemoryPcodeExecutorState(piece.fork()); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetMemoryPcodeExecutorStatePiece.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetMemoryPcodeExecutorStatePiece.java index a800872a14..e80f37baad 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetMemoryPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetMemoryPcodeExecutorStatePiece.java @@ -15,10 +15,12 @@ */ package ghidra.app.plugin.core.debug.service.emulation; +import java.util.Map; import java.util.concurrent.CompletableFuture; import ghidra.app.plugin.core.debug.service.emulation.data.PcodeDebuggerDataAccess; import ghidra.app.plugin.core.debug.service.emulation.data.PcodeDebuggerMemoryAccess; +import ghidra.generic.util.datastruct.SemisparseByteArray; import ghidra.program.model.address.*; import ghidra.program.model.lang.Language; import ghidra.trace.model.memory.TraceMemoryState; @@ -66,6 +68,18 @@ public class RWTargetMemoryPcodeExecutorStatePiece this.backing = backing; } + protected RWTargetMemoryCachedSpace(Language language, AddressSpace space, + PcodeDebuggerMemoryAccess backing, SemisparseByteArray bytes, AddressSet written) { + super(language, space, backing, bytes, written); + this.backing = backing; + } + + @Override + public RWTargetMemoryCachedSpace fork() { + return new RWTargetMemoryCachedSpace(language, space, backing, bytes.fork(), + new AddressSet(written)); + } + @Override protected void fillUninitialized(AddressSet uninitialized) { if (space.isUniqueSpace()) { @@ -114,14 +128,29 @@ public class RWTargetMemoryPcodeExecutorStatePiece this.mode = mode; } + class WRTargetMemorySpaceMap extends TargetBackedSpaceMap { + public WRTargetMemorySpaceMap() { + super(); + } + + protected WRTargetMemorySpaceMap(Map spaceMap) { + super(spaceMap); + } + + @Override + public AbstractSpaceMap fork() { + return new WRTargetMemorySpaceMap(fork(spaces)); + } + + @Override + protected CachedSpace newSpace(AddressSpace space, PcodeDebuggerDataAccess data) { + return new RWTargetMemoryCachedSpace(language, space, + (PcodeDebuggerMemoryAccess) data); + } + } + @Override protected AbstractSpaceMap newSpaceMap() { - return new TargetBackedSpaceMap() { - @Override - protected CachedSpace newSpace(AddressSpace space, PcodeDebuggerDataAccess data) { - return new RWTargetMemoryCachedSpace(language, space, - (PcodeDebuggerMemoryAccess) data); - } - }; + return new WRTargetMemorySpaceMap(); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetRegistersPcodeExecutorState.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetRegistersPcodeExecutorState.java index a9beec92aa..46a7f40ea7 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetRegistersPcodeExecutorState.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetRegistersPcodeExecutorState.java @@ -16,7 +16,7 @@ package ghidra.app.plugin.core.debug.service.emulation; import ghidra.app.plugin.core.debug.service.emulation.data.PcodeDebuggerRegistersAccess; -import ghidra.pcode.exec.trace.DefaultTracePcodeExecutorState; +import ghidra.pcode.exec.trace.*; /** * A state composing a single {@link RWTargetRegistersPcodeExecutorStatePiece} @@ -31,4 +31,14 @@ public class RWTargetRegistersPcodeExecutorState extends DefaultTracePcodeExecut public RWTargetRegistersPcodeExecutorState(PcodeDebuggerRegistersAccess data, Mode mode) { super(new RWTargetRegistersPcodeExecutorStatePiece(data, mode)); } + + protected RWTargetRegistersPcodeExecutorState( + TracePcodeExecutorStatePiece piece) { + super(piece); + } + + @Override + public RWTargetRegistersPcodeExecutorState fork() { + return new RWTargetRegistersPcodeExecutorState(piece.fork()); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetRegistersPcodeExecutorStatePiece.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetRegistersPcodeExecutorStatePiece.java index 2f26847bf1..f09212652a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetRegistersPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/RWTargetRegistersPcodeExecutorStatePiece.java @@ -15,10 +15,12 @@ */ package ghidra.app.plugin.core.debug.service.emulation; +import java.util.Map; import java.util.concurrent.CompletableFuture; import ghidra.app.plugin.core.debug.service.emulation.data.PcodeDebuggerDataAccess; import ghidra.app.plugin.core.debug.service.emulation.data.PcodeDebuggerRegistersAccess; +import ghidra.generic.util.datastruct.SemisparseByteArray; import ghidra.program.model.address.*; import ghidra.program.model.lang.Language; import ghidra.trace.model.memory.TraceMemoryState; @@ -62,6 +64,19 @@ public class RWTargetRegistersPcodeExecutorStatePiece this.backing = backing; } + protected RWTargetRegistersCachedSpace(Language language, AddressSpace space, + PcodeDebuggerRegistersAccess backing, SemisparseByteArray bytes, + AddressSet written) { + super(language, space, backing, bytes, written); + this.backing = backing; + } + + @Override + public RWTargetRegistersCachedSpace fork() { + return new RWTargetRegistersCachedSpace(language, uniqueSpace, backing, bytes.fork(), + new AddressSet(written)); + } + @Override protected void fillUninitialized(AddressSet uninitialized) { if (space.isUniqueSpace()) { @@ -98,14 +113,29 @@ public class RWTargetRegistersPcodeExecutorStatePiece this.mode = mode; } + class WRTargetRegistersSpaceMap extends TargetBackedSpaceMap { + public WRTargetRegistersSpaceMap() { + super(); + } + + protected WRTargetRegistersSpaceMap(Map spaceMap) { + super(spaceMap); + } + + @Override + public AbstractSpaceMap fork() { + return new WRTargetRegistersSpaceMap(fork(spaces)); + } + + @Override + protected CachedSpace newSpace(AddressSpace space, PcodeDebuggerDataAccess data) { + return new RWTargetRegistersCachedSpace(language, space, + (PcodeDebuggerRegistersAccess) data); + } + } + @Override protected AbstractSpaceMap newSpaceMap() { - return new TargetBackedSpaceMap() { - @Override - protected CachedSpace newSpace(AddressSpace space, PcodeDebuggerDataAccess data) { - return new RWTargetRegistersCachedSpace(language, space, - (PcodeDebuggerRegistersAccess) data); - } - }; + return new WRTargetRegistersSpaceMap(); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/PcodeDebuggerAccess.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/PcodeDebuggerAccess.java index 5c39380c4e..79e07561cd 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/PcodeDebuggerAccess.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/emulation/data/PcodeDebuggerAccess.java @@ -17,6 +17,7 @@ package ghidra.app.plugin.core.debug.service.emulation.data; import ghidra.pcode.emu.PcodeThread; import ghidra.pcode.exec.trace.data.PcodeTraceAccess; +import ghidra.trace.model.thread.TraceThread; /** * A trace-and-debugger access shim @@ -34,4 +35,7 @@ public interface PcodeDebuggerAccess extends PcodeTraceAccess { @Override PcodeDebuggerRegistersAccess getDataForLocalState(PcodeThread thread, int frame); + + @Override + PcodeDebuggerRegistersAccess getDataForLocalState(TraceThread thread, int frame); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java index 0e7a1296fd..d8810dd0e8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java @@ -29,6 +29,7 @@ import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; import ghidra.app.plugin.core.debug.event.TraceOpenedPluginEvent; +import ghidra.app.plugin.core.debug.stack.*; import ghidra.app.plugin.core.debug.utils.*; import ghidra.app.services.*; import ghidra.app.services.ModuleMapProposal.ModuleMapEntry; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/AbstractUnwoundFrame.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/AbstractUnwoundFrame.java new file mode 100644 index 0000000000..440aa8965c --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/AbstractUnwoundFrame.java @@ -0,0 +1,359 @@ +/* ### + * 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.stack; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.apache.commons.lang3.ArrayUtils; + +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.services.DebuggerStateEditingService.StateEditor; +import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.async.AsyncFence; +import ghidra.framework.plugintool.PluginTool; +import ghidra.pcode.eval.AbstractVarnodeEvaluator; +import ghidra.pcode.eval.ArithmeticVarnodeEvaluator; +import ghidra.pcode.exec.*; +import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; +import ghidra.pcode.opbehavior.BinaryOpBehavior; +import ghidra.pcode.opbehavior.UnaryOpBehavior; +import ghidra.pcode.utils.Utils; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; +import ghidra.program.model.listing.Program; +import ghidra.program.model.listing.VariableStorage; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.program.model.pcode.Varnode; +import ghidra.program.util.ProgramLocation; +import ghidra.trace.model.Trace; +import ghidra.trace.model.TraceLocation; +import ghidra.trace.model.guest.TracePlatform; + +/** + * An abstract implementation of {@link UnwoundFrame} + * + *

    + * This generally contains all the methods for interpreting and retrieving higher-level variables + * once the frame context is known. It doesn't contain the mechanisms for creating or reading + * annotations. + * + * @param the type of values retrievable from the unwound frame + */ +public abstract class AbstractUnwoundFrame implements UnwoundFrame { + /** + * A class which can evaluate high p-code varnodes in the context of a stack frame using a + * p-code arithmetic + * + * @param the evaluation result type + */ + protected abstract class ArithmeticFrameVarnodeEvaluator + extends ArithmeticVarnodeEvaluator { + public ArithmeticFrameVarnodeEvaluator(PcodeArithmetic arithmetic) { + super(arithmetic); + } + + @Override + protected Address applyBase(long offset) { + return AbstractUnwoundFrame.this.applyBase(offset); + } + + @Override + protected Address translateMemory(Program program, Address address) { + TraceLocation location = mappingService.getOpenMappedLocation(trace, + new ProgramLocation(program, address), snap); + return location == null ? null : location.getAddress(); + } + } + + /** + * A class which can evaluate high p-code varnodes in the context of a stack frame + * + * @param the evaluation result type + */ + protected abstract class AbstractFrameVarnodeEvaluator extends AbstractVarnodeEvaluator { + @Override + protected Address applyBase(long offset) { + return AbstractUnwoundFrame.this.applyBase(offset); + } + + @Override + protected Address translateMemory(Program program, Address address) { + TraceLocation location = mappingService.getOpenMappedLocation(trace, + new ProgramLocation(program, address), snap); + return location == null ? null : location.getAddress(); + } + } + + /** + * A frame evaluator which descends to symbol storage + * + *

    + * This ensure that if a register is used as a temporary value in an varnode AST, that + * evaluation proceeds all the way to the "source" symbols. + * + * @param the evaluation result type + */ + protected abstract class FrameVarnodeEvaluator extends ArithmeticFrameVarnodeEvaluator { + private final AddressSetView symbolStorage; + + /** + * Construct an evaluator with the given arithmetic and symbol storage + * + *

    + * Varnodes contained completely in symbol storage are presumed to be the inputs of the + * evaluation. All other varnodes are evaluated by examining their defining p-code op. It is + * an error to include any unique space in symbol storage. + * + * @param arithmetic the arithmetic for evaluating p-code ops + * @param symbolStorage the address ranges to regard as input, i.e., the leaves of evalution + */ + public FrameVarnodeEvaluator(PcodeArithmetic arithmetic, AddressSetView symbolStorage) { + super(arithmetic); + this.symbolStorage = symbolStorage; + } + + @Override + protected boolean isLeaf(Varnode vn) { + return vn.isConstant() || + symbolStorage.contains(vn.getAddress(), vn.getAddress().add(vn.getSize() - 1)); + } + } + + /** + * A frame "evaluator" which merely gets values + * + *

    + * This evaluator never descends to defining p-code ops. It is an error to ask it for the value + * of unique varnodes. With some creativity, this can also be used as a varnode visitor to set + * values. + * + * @param the evaluation result type + */ + protected abstract class FrameVarnodeValueGetter extends ArithmeticFrameVarnodeEvaluator { + public FrameVarnodeValueGetter(PcodeArithmetic arithmetic) { + super(arithmetic); + } + + @Override + protected boolean isLeaf(Varnode vn) { + return true; + } + } + + /** + * A frame "evaluator" which actually sets values + * + * @param the evaluation result type + */ + protected abstract class FrameVarnodeValueSetter extends AbstractFrameVarnodeEvaluator { + @Override + protected boolean isLeaf(Varnode vn) { + return true; + } + + @Override + protected U evaluateUnaryOp(Program program, PcodeOp op, UnaryOpBehavior unOp, + Map already) { + throw new UnsupportedOperationException(); + } + + @Override + protected U evaluateBinaryOp(Program program, PcodeOp op, BinaryOpBehavior binOp, + Map already) { + throw new UnsupportedOperationException(); + } + + @Override + protected U evaluateAbstract(Program program, AddressSpace space, U offset, int size, + Map already) { + throw new UnsupportedOperationException(); + } + + @Override + protected U evaluateConstant(long value, int size) { + throw new UnsupportedOperationException(); + } + + @Override + protected U evaluateLoad(Program program, PcodeOp op, Map already) { + throw new UnsupportedOperationException(); + } + + @Override + protected U evaluatePtrAdd(Program program, PcodeOp op, Map already) { + throw new UnsupportedOperationException(); + } + + @Override + protected U evaluatePtrSub(Program program, PcodeOp op, Map already) { + throw new UnsupportedOperationException(); + } + } + + protected final DebuggerCoordinates coordinates; + protected final Trace trace; + protected final TracePlatform platform; + protected final long snap; + protected final long viewSnap; + protected final Language language; + protected final AddressSpace codeSpace; + protected final Register pc; + protected final PcodeExecutorState state; + + protected final DebuggerStaticMappingService mappingService; + + /** + * Construct an unwound frame + * + * @param tool the tool requesting interpretation of the frame, which provides context for + * mapped static programs. + * @param coordinates the coordinates (trace, thread, snap, etc.) to examine + * @param state the machine state, typically the watch value state for the same coordinates. It + * is the caller's (i.e., subclass') responsibility to ensure the given state + * corresponds to the given coordinates. + */ + public AbstractUnwoundFrame(PluginTool tool, DebuggerCoordinates coordinates, + PcodeExecutorState state) { + this.coordinates = coordinates; + this.trace = coordinates.getTrace(); + this.platform = coordinates.getPlatform(); + this.snap = coordinates.getSnap(); + this.viewSnap = coordinates.getViewSnap(); + this.language = platform.getLanguage(); + this.codeSpace = language.getDefaultSpace(); + this.pc = language.getProgramCounter(); + + this.state = state; + this.mappingService = tool.getService(DebuggerStaticMappingService.class); + } + + /** + * Get or recover the saved register map + * + *

    + * This indicates the location of saved registers on the stack that apply to this frame. + * + * @return the register map + */ + protected abstract SavedRegisterMap computeRegisterMap(); + + /** + * Compute the address of the return address + * + * @return the address of the return address + */ + protected abstract Address computeAddressOfReturnAddress(); + + /** + * Compute the address (in physical stack space) of the given stack offset + * + * @param offset the stack offset, relative to the stack pointer at the entry to the function + * that allocated this frame. + * @return the address in physical stack space + */ + protected abstract Address applyBase(long offset); + + @Override + public T getValue(VariableStorage storage) { + SavedRegisterMap registerMap = computeRegisterMap(); + return new FrameVarnodeValueGetter(state.getArithmetic()) { + @Override + protected T evaluateMemory(Address address, int size) { + return registerMap.getVar(state, address, size, Reason.INSPECT); + } + }.evaluateStorage(storage); + } + + @Override + public T getValue(Register register) { + SavedRegisterMap registerMap = computeRegisterMap(); + return registerMap.getVar(state, register.getAddress(), register.getNumBytes(), + Reason.INSPECT); + } + + @Override + public T evaluate(VariableStorage storage, AddressSetView symbolStorage) { + SavedRegisterMap registerMap = computeRegisterMap(); + return new FrameVarnodeEvaluator(state.getArithmetic(), symbolStorage) { + @Override + protected T evaluateMemory(Address address, int size) { + return registerMap.getVar(state, address, size, Reason.INSPECT); + } + }.evaluateStorage(storage); + } + + @Override + public T evaluate(Program program, PcodeOp op, AddressSetView symbolStorage) { + SavedRegisterMap registerMap = computeRegisterMap(); + return new FrameVarnodeEvaluator(state.getArithmetic(), symbolStorage) { + @Override + protected T evaluateMemory(Address address, int size) { + return registerMap.getVar(state, address, size, Reason.INSPECT); + } + }.evaluateOp(program, op); + } + + @Override + public CompletableFuture setValue(StateEditor editor, VariableStorage storage, + BigInteger value) { + SavedRegisterMap registerMap = computeRegisterMap(); + ByteBuffer buf = ByteBuffer.wrap(Utils.bigIntegerToBytes(value, storage.size(), true)); + AsyncFence fence = new AsyncFence(); + new FrameVarnodeValueSetter() { + @Override + protected ByteBuffer evaluateMemory(Address address, int size) { + byte[] bytes = new byte[size]; + buf.get(bytes); + if (!language.isBigEndian()) { + ArrayUtils.reverse(bytes); + } + fence.include(registerMap.setVar(editor, address, bytes)); + return buf; + } + + @Override + protected ByteBuffer catenate(int total, ByteBuffer value, ByteBuffer piece, int size) { + return value; + } + + @Override + public ByteBuffer evaluateStorage(VariableStorage storage) { + return evaluateStorage(storage, buf); + } + }.evaluateStorage(storage); + return fence.ready(); + } + + @Override + public CompletableFuture setReturnAddress(StateEditor editor, Address addr) { + if (addr.getAddressSpace() != codeSpace) { + throw new IllegalArgumentException("Return address must be in " + codeSpace); + } + BytesPcodeArithmetic bytesArithmetic = BytesPcodeArithmetic.forLanguage(language); + byte[] bytes = bytesArithmetic.fromConst(addr.getOffset(), pc.getNumBytes()); + return editor.setVariable(computeAddressOfReturnAddress(), bytes); + } + + @Override + public T zext(T value, int length) { + PcodeArithmetic arithmetic = state.getArithmetic(); + return arithmetic.unaryOp(PcodeOp.INT_ZEXT, length, (int) arithmetic.sizeOf(value), value); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/AnalysisUnwoundFrame.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/AnalysisUnwoundFrame.java new file mode 100644 index 0000000000..faadeedafb --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/AnalysisUnwoundFrame.java @@ -0,0 +1,400 @@ +/* ### + * 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.stack; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.CompletableFuture; + +import ghidra.app.plugin.core.bookmark.BookmarkNavigator; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.services.DebuggerStateEditingService.StateEditor; +import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.framework.plugintool.PluginTool; +import ghidra.pcode.exec.BytesPcodeArithmetic; +import ghidra.pcode.exec.PcodeExecutorState; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRangeImpl; +import ghidra.program.model.data.DataTypeConflictHandler; +import ghidra.program.model.data.Structure; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.SourceType; +import ghidra.program.model.util.CodeUnitInsertionException; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.bookmark.*; +import ghidra.trace.model.listing.TraceData; +import ghidra.trace.model.symbol.TraceReferenceManager; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A frame recovered from analysis of a thread's register bank and stack segment + * + *

    + * The typical pattern for invoking analysis to unwind an entire stack is to use + * {@link StackUnwinder#start(DebuggerCoordinates, TaskMonitor)} or similar, followed by + * {@link #unwindNext(TaskMonitor)} in a chain until the stack is exhausted or analysis fails to + * unwind a frame. It may be more convenient to use + * {@link StackUnwinder#frames(DebuggerCoordinates, TaskMonitor)}. Its iterator implements that + * pattern. Because unwinding can be expensive, it is recommended to cache the unwound stack when + * possible. A centralized service for stack unwinding may be added later. + * + * @param the type of values retrievable from the unwound frame + */ +public class AnalysisUnwoundFrame extends AbstractUnwoundFrame { + + private final StackUnwinder unwinder; + private final int level; + private final Address pcVal; + private final Address spVal; + private final UnwindInfo info; + private final UnwindException infoErr; + private final SavedRegisterMap registerMap; + + private final Address base; + + /** + * Construct an unwound frame + * + *

    + * Clients should instead use {@link StackUnwinder#start(DebuggerCoordinates, TaskMonitor)} or + * similar, or {@link #unwindNext(TaskMonitor)}. + * + * @param tool the tool requesting interpretation of the frame, which provides context for + * mapped static programs. + * @param coordinates the coordinates (trace, thread, snap, etc.) to examine + * @param unwinder the unwinder that produced this frame, and may be used to unwind the next + * frame + * @param state the machine state, typically the watch value state for the same coordinates. It + * is the caller's (i.e., subclass') responsibility to ensure the given state + * corresponds to the given coordinates. + * @param level the level of this frame + * @param pcVal the address of the next instruction when this frame becomes the current frame + * @param spVal the address of the top of the stack when this frame becomes the current frame + * @param info the information used to unwind this frame + * @param infoErr if applicable, an error describing why the unwind info is missing or + * incomplete + * @param registerMap a map from registers to the offsets of their saved values on the stack + */ + AnalysisUnwoundFrame(PluginTool tool, DebuggerCoordinates coordinates, StackUnwinder unwinder, + PcodeExecutorState state, int level, Address pcVal, Address spVal, UnwindInfo info, + UnwindException infoErr, SavedRegisterMap registerMap) { + super(tool, coordinates, state); + if ((info == null) == (infoErr == null)) { + throw new AssertionError(); + } + this.unwinder = unwinder; + this.level = level; + + this.pcVal = pcVal; + this.spVal = spVal; + this.info = info; + this.infoErr = infoErr; + this.registerMap = registerMap; + + if (info != null) { + this.base = info.computeBase(spVal); + } + else { + this.base = null; + } + } + + @Override + public boolean isFake() { + return false; + } + + /** + * Unwind the next frame up + * + *

    + * Unwind the frame that would become current if the function that allocated this frame were to + * return. For example, if this frame is at level 3, {@code unwindNext} will attempt to unwind + * the frame at level 4. + * + *

    + * The program counter and stack pointer for the next frame are computed using the state + * originally given in + * {@link StackUnwinder#start(DebuggerCoordinates, PcodeExecutorState, TaskMonitor)} and this + * frame's unwind information. The state is usually the watch-value state bound to the starting + * coordinates. The program counter is evaluated like any other variable. The stack pointer is + * computed by removing the depth of this frame. Then registers are restored and unwinding + * proceeds the same as the starting frame. + * + * @param monitor a monitor for cancellation + * @return the next frame up + * @throws CancelledException if the monitor is cancelled + * @throws UnwindException if unwinding fails + */ + public AnalysisUnwoundFrame unwindNext(TaskMonitor monitor) throws CancelledException { + if (info == null || info.ofReturn() == null) { + throw new NoSuchElementException(); + } + SavedRegisterMap registerMap = this.registerMap.fork(); + info.mapSavedRegisters(base, registerMap); + Address pcVal = info.computeNextPc(base, state, codeSpace, pc); + Address spVal = info.computeNextSp(base); + return unwinder.unwind(coordinates, level + 1, pcVal, spVal, state, registerMap, + monitor); + } + + @Override + protected Address applyBase(long offset) { + if (base == null) { + throw new UnwindException("Cannot compute stack address for offset " + offset, + infoErr); + } + return base.add(offset); + } + + @Override + protected SavedRegisterMap computeRegisterMap() { + return registerMap; + } + + @Override + protected Address computeAddressOfReturnAddress() { + return info.ofReturn(base); + } + + @Override + public Address getReturnAddress() { + return info.computeNextPc(base, state, codeSpace, pc); + } + + @Override + public CompletableFuture setReturnAddress(StateEditor editor, Address addr) { + if (addr.getAddressSpace() != codeSpace) { + throw new IllegalArgumentException("Return address must be in " + codeSpace); + } + BytesPcodeArithmetic bytesArithmetic = BytesPcodeArithmetic.forLanguage(language); + byte[] bytes = bytesArithmetic.fromConst(addr.getOffset(), pc.getNumBytes()); + return editor.setVariable(info.ofReturn(base), bytes); + } + + @Override + public int getLevel() { + return level; + } + + @Override + public String getDescription() { + return String.format("%s %s pc=%s sp=%s base=%s", level, info.function(), + pcVal.toString(false), spVal.toString(false), base.toString(false)); + } + + @Override + public Address getBasePointer() { + return base; + } + + @Override + public Address getProgramCounter() { + return pcVal; + } + + public Address getStackPointer() { + return spVal; + } + + @Override + public Function getFunction() { + return info.function(); + } + + /** + * Generate the structure for {@link #resolveStructure(int)} + * + * @param prevParamSize the number of bytes occupied by the parameters for the next frame down. + * @return the generated structure + */ + protected Structure generateStructure(int prevParamSize) { + FrameStructureBuilder builder = + new FrameStructureBuilder(language, pcVal, info, prevParamSize); + return builder.build(StackUnwinder.FRAMES_PATH, "frame_" + pcVal.toString(false), + trace.getDataTypeManager()); + } + + /** + * Create or resolve the structure data type representing this frame + * + *

    + * The structure composes a variety of information: 1) The stack variables (locals and + * parameters) of the function that allocated the frame. Note that some variables may be omitted + * if the function has not allocated them or has already freed them relative to the frame's + * program counter. 2) Saved registers. Callee-saved registers will typically appear closer to + * the next frame up. Caller-saved registers, assuming Ghidra hasn't already assigned the stack + * offset to a local variable, will typically appear close to the next frame down. 3) The return + * address, if on the stack. + * + * @param prevParamSize the number of bytes occupied by the parameters for the next frame down. + * Parameters are pushed by the caller, and so appear to be allocated by the caller; + * however, the really belong to the callee, so this specifies the number of bytes to + * "donate" to the callee's frame. + * @return the structure, to be placed {@code prevParamSize} bytes after the frame's stack + * pointer. + */ + public Structure resolveStructure(int prevParamSize) { + Structure structure = generateStructure(prevParamSize); + return structure == null ? null + : trace.getDataTypeManager() + .addType(structure, DataTypeConflictHandler.DEFAULT_HANDLER); + } + + /** + * Get or create the bookmark type for warnings + * + * @return the bookmark type + */ + protected TraceBookmarkType getWarningBookmarkType() { + TraceBookmarkType type = trace.getBookmarkManager().getBookmarkType(BookmarkType.WARNING); + if (type != null) { + return type; + } + BookmarkNavigator.defineBookmarkTypes(trace.getProgramView()); + return trace.getBookmarkManager().getBookmarkType(BookmarkType.WARNING); + } + + protected static void truncateOrDelete(TraceBookmark tb, Lifespan remove) { + List newLifespan = tb.getLifespan().subtract(remove); + if (newLifespan.isEmpty()) { + tb.delete(); + } + else { + tb.setLifespan(newLifespan.get(0)); + } + } + + /** + * Apply this unwound frame to the trace's listing + * + *

    + * This performs the following, establishing some conventions for trace stack analysis: + *

      + *
    • Places a bookmark at the frame start indicating any warnings encountered while analyzing + * it.
    • + *
    • Places a structure at (or near) the derived stack pointer whose fields denote the various + * stack entries: local variables, saved registers, return address, parameters. The structure + * may be placed a little after the derived stack pointer to accommodate the parameters of an + * inner stack frame. The structure data type will have the category path + * {@link StackUnwinder#FRAMES_PATH}. This allows follow-on analysis to identify data units + * representing unwound frames. See {@link #isFrame(TraceData)}.
    • + *
    • Places a comment at the start of the frame. This is meant for human consumption, so + * follow-on analysis should not attempt to parse or otherwise interpret it. It will indicate + * the frame level (0 being the innermost), the function name, the program counter, the stack + * pointer, and the frame base pointer.
    • + *
    • Places a {@link RefType#DATA} reference from the frame start to its own base address. + * This permits follow-on analysis to derive variable values stored on the stack. See + * {@link #getBase(TraceData)} and {@link #getValue(TraceData, VariableStorage)}.
    • + *
    • Places a {@link RefType#DATA} reference from the program counter to the frame start. This + * allows follow-on analysis to determine the function for the frame. See + * {@link #getProgramCounter(TraceData)} and + * {@link #getFunction(TraceData, DebuggerStaticMappingService)}.
    • + *
    + * + *

    + * The resulting data unit can be retrieved from the trace database and later used to construct + * a {@link ListingUnwoundFrame}. If the frame structure would have length 0 it is not applied. + * + * @param prevParamSize the number of bytes occupied by the parameters for the next frame down. + * See {@link #resolveStructure(int)}. + * @param monitor a monitor for cancellation + * @return the data unit for the frame structure applied, or null + * @throws CancelledException if the monitor is cancelled + */ + public TraceData applyToListing(int prevParamSize, TaskMonitor monitor) + throws CancelledException { + // TODO: Positive stack growth + Address spPlusParams = spVal.add(prevParamSize); + TraceBookmarkManager bm = trace.getBookmarkManager(); + TraceBookmarkType btWarn = getWarningBookmarkType(); + Lifespan span = Lifespan.nowOnMaybeScratch(viewSnap); + String warnings = info.warnings().summarize(); + Structure structure = resolveStructure(prevParamSize); + if (structure == null || structure.isZeroLength()) { + for (TraceBookmark existing : bm.getBookmarksAt(viewSnap, spPlusParams)) { + truncateOrDelete(existing, span); + } + bm.addBookmark(span, spPlusParams, btWarn, "Stack Unwind", + "Frame " + level + " has lenght 0"); + return null; + } + for (TraceBookmark existing : bm.getBookmarksIntersecting(span, + new AddressRangeImpl(spPlusParams, spPlusParams.add(structure.getLength() - 1)))) { + truncateOrDelete(existing, span); + } + if (!warnings.isBlank()) { + bm.addBookmark(span, spPlusParams, btWarn, "Unwind Stack", warnings); + } + + try { + trace.getCodeManager() + .definedUnits() + .clear(Lifespan.at(viewSnap), new AddressRangeImpl(spPlusParams, + spPlusParams.add(structure.getLength() - 1)), false, monitor); + TraceData frame = trace.getCodeManager() + .definedData() + .create(span, spPlusParams, structure); + frame.setComment(CodeUnit.PRE_COMMENT, getDescription()); + TraceReferenceManager refs = trace.getReferenceManager(); + refs.clearReferencesFrom(span, frame.getRange()); + refs.clearReferencesTo(span, frame.getRange()); + frame.addOperandReference(StackUnwinder.BASE_OP_INDEX, base, RefType.DATA, + SourceType.ANALYSIS); + refs.addMemoryReference(span, pcVal, spPlusParams, RefType.DATA, SourceType.ANALYSIS, + StackUnwinder.PC_OP_INDEX); + return frame; + } + catch (CodeUnitInsertionException e) { + throw new AssertionError(e); + } + } + + /** + * Get the unwind information from the analysis used to unwind this frame + * + * @return the information + */ + public UnwindInfo getUnwindInfo() { + return info; + } + + /** + * If the unwind information is absent or incomplete, get the error explaining why. + * + *

    + * When analysis is incomplete, the frame may still be partially unwound, meaning only certain + * variables can be evaluated, and the return address may not be available. Typically, a + * partially unwound frame is the last frame that can be recovered in the stack. If the base + * pointer could not be recovered, then only register variables and static variables can be + * evaluated. + * + * @return the error + */ + public UnwindException getError() { + return infoErr; + } + + @Override + public String getWarnings() { + if (info == null) { + return ""; + } + return info.warnings().summarize(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/FakeUnwoundFrame.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/FakeUnwoundFrame.java new file mode 100644 index 0000000000..52bcf74c74 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/FakeUnwoundFrame.java @@ -0,0 +1,108 @@ +/* ### + * 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.stack; + +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.framework.plugintool.PluginTool; +import ghidra.pcode.exec.PcodeExecutorState; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; + +/** + * A fake frame which can be used to evaluate variables for which an actual frame is not necessary + * or not available. + * + *

    + * This "frame" can only evaluate static / global variables. Neither register variables nor stack + * variables can be evaluated. The reason for excluding registers is because some register values + * may be saved to the stack, so the values in the bank may not be the correct value in the context + * of a given stack frame. Based on an inspection of a variable's storage, it may not be necessary + * to attempt a stack unwind to evaluate it. If that is the case, this "frame" may be used to + * evaluate it where a frame interface is expected or convenient. + */ +public class FakeUnwoundFrame extends AbstractUnwoundFrame { + private static final SavedRegisterMap IDENTITY_MAP = new SavedRegisterMap(); + + /** + * Construct a fake "frame" + * + * @param tool the tool requesting interpretation of the frame, which provides context for + * mapped static programs. + * @param coordinates the coordinates (trace, thread, snap, etc.) to examine + * @param state the machine state, typically the watch value state for the same coordinates. It + * is the caller's (i.e., subclass') responsibility to ensure the given state + * corresponds to the given coordinates. + */ + public FakeUnwoundFrame(PluginTool tool, DebuggerCoordinates coordinates, + PcodeExecutorState state) { + super(tool, coordinates, state); + } + + @Override + public boolean isFake() { + return true; + } + + @Override + public int getLevel() { + throw new UnsupportedOperationException(); + } + + @Override + public String getDescription() { + return "(No frame required)"; + } + + @Override + public Address getProgramCounter() { + return null; + } + + @Override + public Function getFunction() { + return null; + } + + @Override + public Address getBasePointer() { + return null; + } + + @Override + public Address getReturnAddress() { + return null; + } + + @Override + public String getWarnings() { + return ""; + } + + @Override + protected SavedRegisterMap computeRegisterMap() { + return IDENTITY_MAP; + } + + @Override + protected Address computeAddressOfReturnAddress() { + return null; + } + + @Override + protected Address applyBase(long offset) { + throw new UnsupportedOperationException(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/FrameStructureBuilder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/FrameStructureBuilder.java new file mode 100644 index 0000000000..458f4351f7 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/FrameStructureBuilder.java @@ -0,0 +1,278 @@ +/* ### + * 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.stack; + +import java.util.*; +import java.util.Map.Entry; + +import generic.ULongSpan; +import generic.ULongSpan.DefaultULongSpanSet; +import generic.ULongSpan.MutableULongSpanSet; +import ghidra.program.model.address.*; +import ghidra.program.model.data.*; +import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; +import ghidra.program.model.listing.*; +import ghidra.program.model.pcode.Varnode; +import ghidra.trace.model.data.TraceBasedDataTypeManager; +import ghidra.util.Msg; + +/** + * The implementation of {@link AnalysisUnwoundFrame#generateStructure(int)} + */ +class FrameStructureBuilder { + public static final String RETURN_ADDRESS_FIELD_NAME = "return_address"; + public static final String SAVED_REGISTER_FIELD_PREFIX = "saved_"; + + private record FrameField(Address address, String name, DataType type, int length, + int scopeStart) { + AddressRange range() { + return new AddressRangeImpl(address, address.add(length - 1)); + } + + boolean overlaps(FrameStructureBuilder.FrameField that) { + return range().intersects(that.range()); + } + } + + private final AddressSpace codeSpace; + private final Register pc; + private final Address min; + private Address max; // Exclusive + private final long functionOffset; + private final NavigableMap fields = new TreeMap<>(); + + /** + * Builder for a structure based on unwind info and function stack variables + * + * @param language the language defining the program counter register and code address space + * @param pcVal the value of the program counter, used to determine variable scope + * @param info the unwind information + * @param prevParamSize the number of bytes past the stack pointer at entry used by the previous + * frame, typically for its parameters + */ + FrameStructureBuilder(Language language, Address pcVal, UnwindInfo info, int prevParamSize) { + this.codeSpace = language.getDefaultSpace(); + this.pc = language.getProgramCounter(); + this.min = info.function() + .getProgram() + .getAddressFactory() + .getStackSpace() + .getAddress(info.depth() + prevParamSize); + this.max = min; + this.functionOffset = pcVal.subtract(info.function().getEntryPoint()); + processSaved(info.saved()); + if (info.ofReturn() != null) { + processOfReturn(info.ofReturn()); + } + processFunction(info.function()); + } + + /** + * Remove overlapping fields + * + *

    + * Entries with later {@link FrameField#scopeStart()} are preferred. No fields are generated for + * variables whose scope starts come after the current instruction offset, so that should ensure + * the most relevant variables are selected. Variables are always preferred over saved + * registers. Ideally, the return address should not conflict, but if it does, the return + * address is preferred, esp., since it is necessary to unwind the next frame. + * + * @return the list of non-overlapping fields + */ + protected List resolveOverlaps() { + List result = new ArrayList<>(fields.size()); + Entry ent1 = fields.pollFirstEntry(); + next1: while (ent1 != null) { + FrameStructureBuilder.FrameField field1 = ent1.getValue(); + next2: while (true) { + Entry ent2 = fields.pollFirstEntry(); + if (ent2 == null) { + result.add(field1); + return result; + } + FrameStructureBuilder.FrameField field2 = ent2.getValue(); + if (!field1.overlaps(field2)) { + result.add(field1); + ent1 = ent2; + continue next1; + } + if (field1.scopeStart() > field2.scopeStart()) { + // Drop field2, but we still need to check if field1 overlaps the next + continue next2; + } + else if (field1.scopeStart() < field2.scopeStart()) { + // Drop field1 + ent1 = ent2; + continue next1; + } + else { + Msg.warn(this, + "Two overlapping variables with equal first use offsets...."); + // Prefer field1, I guess + continue next2; + } + } + } + return result; + } + + /** + * Build the resulting structure + * + * @param path the category path for the new structure + * @param name the name of the new structure + * @param dtm the data type manager for the structure + * @return the new structure + */ + public Structure build(CategoryPath path, String name, TraceBasedDataTypeManager dtm) { + List resolved = resolveOverlaps(); + if (resolved.isEmpty()) { + return null; + } + int length = (int) max.subtract(min); + if (length == 0) { + return null; + } + Structure structure = new StructureDataType(path, name, length, dtm); + MutableULongSpanSet undefined = new DefaultULongSpanSet(); + undefined.add(ULongSpan.extent(0, structure.getLength())); + for (FrameStructureBuilder.FrameField field : resolved) { + int offset = (int) field.address().subtract(min); + if (offset < 0) { + /** + * No function should reach beyond the current stack pointer, especially near a + * call, since that space is presumed to belong to the callee. When we see variables + * beyond the current depth, it's likely they're just not in scope. For example, a + * local variable may be allocated on the stack when entering a block, so Ghidra + * will show that variable in the frame. However, we may encounter a program counter + * during unwinding that has not entered that block, or if it did, has already + * exited and deallocated the local. We must omit such variables. NOTE: For some + * variables, notably those that re-use storage, we do note the start of it scope, + * but not the end. + * + * The min also accounts for the parameters in the previous frame. We'll observe + * writes to those, but we shouldn't see reads. Depending on changes to the unwind + * static analysis, passed parameters could get mistaken for saved registers. + * Nevertheless, we should prefer to assign overlapped portions of frames to the + * frame that uses them for parameters rather than scratch space. If diagnostics are + * desired, we may need to distinguish min from the SP vs min from SP + prevParams + * so that we can better assess each conflict. + */ + continue; + } + DataType type = field.type(); + if (type == IntegerDataType.dataType) { + type = IntegerDataType.getUnsignedDataType(field.length(), dtm); + } + else if (type == PointerDataType.dataType) { + type = new PointerTypedefBuilder(PointerDataType.dataType, dtm) + .addressSpace(codeSpace) + .build(); + } + structure.replaceAtOffset(offset, type, field.length(), field.name(), ""); + undefined.remove(ULongSpan.extent(offset, field.length())); + } + for (ULongSpan undefSpan : undefined.spans()) { + int spanLength = (int) undefSpan.length(); + DataType type = new ArrayDataType(DataType.DEFAULT, spanLength, 1, dtm); + int offset = undefSpan.min().intValue(); + Address addr = min.add(offset); + String fieldName = addr.getOffset() < 0 + ? String.format("offset_0x%x", -addr.getOffset()) + : String.format("posOff_0x%x", addr.getOffset()); + structure.replaceAtOffset(offset, type, spanLength, fieldName, ""); + } + return structure; + } + + void processVar(Address address, String name, DataType type, int length, int scopeStart) { + if (!address.isStackAddress()) { + return; + } + Address varMax = address.add(length); + if (varMax.compareTo(max) > 0) { + max = varMax; + } + fields.put(address, new FrameField(address, name, type, length, scopeStart)); + } + + void processRegisterVar(Address address, String name, DataType dataType, + Register register, int scopeStart) { + processVar(address, name, dataType, register.getNumBytes(), scopeStart); + } + + void processSavedRegister(Address address, Register register) { + processRegisterVar(address, SAVED_REGISTER_FIELD_PREFIX + register.getName(), + IntegerDataType.dataType, + register, -1); + } + + void processSaved(Map saved) { + for (Entry entry : saved.entrySet()) { + processSavedRegister(entry.getValue(), entry.getKey()); + } + } + + void processOfReturn(Address address) { + processRegisterVar(address, RETURN_ADDRESS_FIELD_NAME, PointerDataType.dataType, pc, + Integer.MAX_VALUE); + } + + void processFunction(Function function) { + for (Variable variable : function.getStackFrame().getStackVariables()) { + if (variable.getFirstUseOffset() > functionOffset) { + continue; + } + processVariable(variable); + } + } + + String prependIfAbsent(String prefix, String name) { + if (name.startsWith(prefix)) { + return name; + } + return prefix + name; + } + + void processVariable(Variable variable) { + Varnode[] varnodes = variable.getVariableStorage().getVarnodes(); + String name = variable.getName(); + if (variable instanceof Parameter) { + name = prependIfAbsent("param_", name); + } + else if (variable instanceof LocalVariable) { + name = prependIfAbsent("local_", name); + } + else { + throw new AssertionError(); + } + if (varnodes.length == 1) { + processVarnode(name, variable.getDataType(), varnodes[0], + variable.getFirstUseOffset()); + } + else { + for (int i = 0; i < varnodes.length; i++) { + processVarnode(name + "_pt" + i, IntegerDataType.dataType, varnodes[i], + variable.getFirstUseOffset()); + } + } + } + + void processVarnode(String name, DataType type, Varnode vn, int scopeStart) { + processVar(vn.getAddress(), name, type, vn.getSize(), scopeStart); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/ListingUnwoundFrame.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/ListingUnwoundFrame.java new file mode 100644 index 0000000000..51f8b06055 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/ListingUnwoundFrame.java @@ -0,0 +1,348 @@ +/* ### + * 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.stack; + +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.framework.plugintool.PluginTool; +import ghidra.pcode.exec.DebuggerPcodeUtils; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValue; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.DataType; +import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.Reference; +import ghidra.program.util.ProgramLocation; +import ghidra.trace.model.DefaultTraceLocation; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.bookmark.TraceBookmark; +import ghidra.trace.model.listing.TraceData; +import ghidra.trace.model.symbol.TraceReference; +import ghidra.trace.util.TraceRegisterUtils; +import ghidra.util.Msg; +import ghidra.util.task.TaskMonitor; + +/** + * A frame restored from annotations applied to the trace listing + * + *

    + * This frame operates on {@link WatchValue}s, which are more than sufficient for most GUI elements. + * The unwinding and display of abstract values introduced by custom emulators is yet to be + * complete. + * + *

    + * This class may become deprecated. It allowed the GUI to use existing analysis that had been + * annotated in this listing. Certainly, that feature will remain, since the annotations are human + * consumable and help make sense of the stack segment. However, when other features need stack + * frames, they may or may not pull those frames from the listing. The trouble comes when a frame + * has 0 length. This can happen when a function has not pushed anything to the stack. On + * architectures without link registers, it should only happen in contingent cases, e.g., the + * analyzer can't find an exit path from the function, and so the return address location is not + * known. However, an invocation of a leaf function on an architecture with a link register may in + * fact have a 0-length frame for its entire life. Ghidra does not cope well with 0-length + * structures, and for good reason. Thus, in most cases, it is recommended to unwind, using + * {@link StackUnwinder}, and cache frames for later re-use. That pattern may be encapsulated in a + * centralized service later. + * + * @see AnalysisUnwoundFrame#applyToListing(int, TaskMonitor) + */ +public class ListingUnwoundFrame extends AbstractUnwoundFrame { + + /** + * Check if the given data unit conventionally represents a frame + * + *

    + * This is a simple conventional check, but it should rule out accidents. It checks that the + * unit's data type belongs to the {@link StackUnwinder#FRAMES_PATH} category. If the user or + * something else puts data types in that category, it's likely data units using those types may + * be mistaken for frames.... + * + * @param data the candidate frame + * @return true if it is likely a frame + */ + public static boolean isFrame(TraceData data) { + DataType type = data.getDataType(); + return type.getCategoryPath().equals(StackUnwinder.FRAMES_PATH); + } + + /** + * Get the conventional level of the frame represented by the givn data unit + * + *

    + * Technically, this violates the convention a little in that it parses the comment to determine + * the frame's level. One alternative is to examine the upper (or lower for positive growth) + * addresses for other frames, but that might require strict absence of gaps between frames. + * Another alternative is to encode the level into a property instead, which is more hidden from + * the user. + * + * @param data the frame + * @return the level + */ + private static Integer getLevel(TraceData data) { + // TODO: Should this go into a property instead? + String comment = data.getComment(CodeUnit.PRE_COMMENT); + if (comment == null) { + return null; + } + String[] parts = comment.split("\\s+"); + if (parts.length == 0) { + return null; + } + try { + return Integer.parseInt(parts[0]); + } + catch (NumberFormatException e) { + return null; + } + } + + private final TraceData frame; + + private final int level; + private final Address pcVal; + private final Function function; + private final Address base; + + private SavedRegisterMap registerMap; + + /** + * Recover a frame from annotations already in the trace listing + * + * @param tool the tool requesting interpretation of the frame, which provides context for + * mapped static programs. + * @param coordinates the coordinates (trace, thread, snap, etc.) to examine + * @param frame the data unit representing the frame + */ + public ListingUnwoundFrame(PluginTool tool, DebuggerCoordinates coordinates, TraceData frame) { + // NOTE: Always unwinding from frame 0 + super(tool, coordinates, DebuggerPcodeUtils.buildWatchState(tool, coordinates.frame(0))); + if (!isFrame(frame)) { + throw new IllegalArgumentException("frame does not appear to represent a frame"); + } + this.frame = frame; + + this.level = loadLevel(); + this.pcVal = loadProgramCounter(); + this.function = loadFunction(); + this.base = loadBasePointer(); + } + + @Override + public boolean isFake() { + return false; + } + + /** + * Get the data unit representing this frame + * + * @return the data unit + */ + public TraceData getData() { + return frame; + } + + @Override + protected Address applyBase(long offset) { + return base.add(offset); + } + + private int loadLevel() { + Integer l = getLevel(frame); + if (l == null) { + throw new IllegalStateException("Frame has no comment indicating its level"); + } + return l; + } + + private Address loadProgramCounter() { + for (Reference ref : frame.getReferenceIteratorTo()) { + if (ref.getReferenceType() != RefType.DATA) { + continue; + } + if (ref.getOperandIndex() != StackUnwinder.PC_OP_INDEX) { + continue; + } + return ref.getFromAddress(); + } + throw new UnwindException("The program counter reference is missing for the frame!"); + } + + private Function loadFunction() { + ProgramLocation staticLoc = + mappingService.getOpenMappedLocation(new DefaultTraceLocation(frame.getTrace(), null, + Lifespan.at(coordinates.getSnap()), pcVal)); + if (staticLoc == null) { + throw new UnwindException( + "The program containing the frame's function is unavailable," + + " or the mappings have changed."); + } + Function function = staticLoc.getProgram() + .getFunctionManager() + .getFunctionContaining(staticLoc.getAddress()); + if (function == null) { + throw new UnwindException( + "The function for the frame is no longer present in the mapped program."); + } + return function; + } + + private Address loadBasePointer() { + for (TraceReference ref : frame.getOperandReferences(StackUnwinder.BASE_OP_INDEX)) { + if (ref.getReferenceType() != RefType.DATA) { + continue; + } + return ref.getToAddress(); + } + return null; + } + + @Override + public String getDescription() { + return frame.getComment(CodeUnit.PRE_COMMENT); + } + + @Override + public int getLevel() { + return level; + } + + @Override + public Address getProgramCounter() { + return pcVal; + } + + @Override + public Function getFunction() { + return function; + } + + @Override + public Address getBasePointer() { + return base; + } + + @Override + protected Address computeAddressOfReturnAddress() { + int numComponents = frame.getNumComponents(); + for (int i = 0; i < numComponents; i++) { + TraceData component = frame.getComponent(i); + if (FrameStructureBuilder.RETURN_ADDRESS_FIELD_NAME.equals(component.getFieldName())) { + return component.getMinAddress(); + } + } + return null; + } + + @Override + public Address getReturnAddress() { + int numComponents = frame.getNumComponents(); + for (int i = 0; i < numComponents; i++) { + TraceData component = frame.getComponent(i); + if (FrameStructureBuilder.RETURN_ADDRESS_FIELD_NAME.equals(component.getFieldName())) { + Object value = component.getValue(); + if (value instanceof Address returnAddress) { + return returnAddress; + } + } + } + return null; + } + + @Override + public String getWarnings() { + for (TraceBookmark bookmark : frame.getTrace() + .getBookmarkManager() + .getBookmarksAt(frame.getStartSnap(), frame.getMinAddress())) { + if (bookmark.getTypeString().equals(BookmarkType.WARNING)) { + return bookmark.getComment(); + } + } + return null; + } + + @Override + protected synchronized SavedRegisterMap computeRegisterMap() { + if (registerMap != null) { + return registerMap; + } + TraceData[] innerFrames = new TraceData[level]; + for (TraceData inner : trace.getCodeManager() + .definedData() + .get(viewSnap, frame.getMinAddress(), false)) { + if (inner == frame) { + continue; + } + if (!isFrame(inner)) { + break; + } + Integer il = getLevel(inner); + if (il == null) { + break; + } + innerFrames[il] = inner; + } + registerMap = new SavedRegisterMap(); + for (TraceData inner : innerFrames) { + mapSavedRegisters(language, inner, registerMap); + } + return registerMap; + } + + /** + * Apply the saved registers for a given frame to the given map + * + *

    + * To be used effectively, all inner frames, excluding the desired context frame, must be + * visited from innermost to outermost, i.e, from level 0 to level n-1. This will ensure that + * the nearest saved value is used for each register. If a register was never saved, then its + * value is presumed still in the actual register. + * + * @param language the language defining the registers to map + * @param frame the data unit for the frame whose saved registers to consider + * @param map the map to modify with saved register information + */ + protected static void mapSavedRegisters(Language language, TraceData frame, + SavedRegisterMap map) { + int numComponents = frame.getNumComponents(); + for (int i = 0; i < numComponents; i++) { + TraceData component = frame.getComponent(i); + String name = component.getFieldName(); + if (!name.startsWith(FrameStructureBuilder.SAVED_REGISTER_FIELD_PREFIX)) { + continue; + } + String regName = + name.substring(FrameStructureBuilder.SAVED_REGISTER_FIELD_PREFIX.length()); + Register register = language.getRegister(regName); + if (register == null) { + Msg.warn(ListingUnwoundFrame.class, + "Unknown register name in saved_register field: " + regName); + continue; + } + map.put(TraceRegisterUtils.rangeForRegister(register), component.getRange()); + } + } + + /** + * Get the stack entry containing the given address + * + * @param address the address, must already have base applied + * @return the component, or null + */ + public TraceData getComponentContaining(Address address) { + return frame.getComponentContaining((int) address.subtract(frame.getMinAddress())); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SavedRegisterMap.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SavedRegisterMap.java new file mode 100644 index 0000000000..923c47ed6c --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SavedRegisterMap.java @@ -0,0 +1,333 @@ +/* ### + * 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.stack; + +import java.nio.ByteBuffer; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; + +import ghidra.app.services.DebuggerStateEditingService.StateEditor; +import ghidra.async.AsyncFence; +import ghidra.pcode.eval.ArithmeticVarnodeEvaluator; +import ghidra.pcode.exec.PcodeArithmetic; +import ghidra.pcode.exec.PcodeExecutorState; +import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.Register; +import ghidra.program.model.pcode.Varnode; +import ghidra.trace.database.DBTraceUtils.AddressRangeMapSetter; +import ghidra.trace.util.TraceRegisterUtils; + +/** + * A map from registers to physical stack addresses + * + *

    + * This is used by an unwound frame to ensure that register reads are translated to stack reads when + * the register's value was saved to the stack by some inner frame. If a register is not saved to + * the stack by such a frame, then its value is read from the register bank. + */ +public class SavedRegisterMap { + /** + * An entry in the map + */ + record SavedEntry(AddressRange from, Address to) { + /** + * The range in register space to be redirected to the stack + * + * @return the "from" range + */ + public AddressRange from() { + return from; + } + + /** + * The physical address in the stack segment to which the register is redirected + * + *

    + * The length of the "to" range is given by the length of the "from" range + * + * @return the "to" address + */ + public Address to() { + return to; + } + + /** + * Check if an access should be redirected according to this entry + * + * @param address the address to be accessed + * @return true to redirect, false otherwise + */ + boolean contains(Address address) { + return from.contains(address); + } + + /** + * Produce an equivalent entry that redirects only the given new "from" range + * + * @param range the new "from" range, which must be enclosed by the current "from" range + * @return the same or truncated entry + */ + public SavedEntry truncate(AddressRange range) { + int right = (int) from.getMaxAddress().subtract(range.getMaxAddress()); + if (right < 0) { + throw new AssertionError("Cannot grow"); + } + int left = (int) from.getMinAddress().subtract(range.getMinAddress()); + if (left < 0) { + throw new AssertionError("Cannot grow"); + } + if (left == 0 && right == 0) { + return this; + } + return new SavedEntry(range, to.add(left)); + } + + /** + * Produce the same or equivalent entry that redirects at most the given "from" range + * + * @param range the "from" range to intersect + * @return the same or truncated entry + */ + public SavedEntry intersect(AddressRange range) { + AddressRange intersection = from.intersect(range); + return intersection == null ? null : truncate(intersection); + } + + /** + * Produce an equivalent entry which excludes any "from" address beyond the given max + * + * @param max the max "from" address + * @return the same or truncated entry + */ + public SavedEntry truncateMax(Address max) { + if (from.getMaxAddress().compareTo(max) <= 0) { + return this; + } + if (from.getMinAddress().compareTo(max) <= 0) { + return truncate(new AddressRangeImpl(from.getMinAddress(), max)); + } + return null; + } + + /** + * Produce an equivalent entry which exclude any "from" address before the given min + * + * @param min the min "from" address + * @return the same or truncated entry + */ + public SavedEntry truncateMin(Address min) { + if (from.getMinAddress().compareTo(min) >= 0) { + return this; + } + if (from.getMaxAddress().compareTo(min) >= 0) { + return truncate(new AddressRangeImpl(min, from.getMaxAddress())); + } + return null; + } + + /** + * The length of the mapped ranges + * + * @return the length + */ + public int size() { + return (int) from.getLength(); + } + } + + /** + * A class which can set values over a range, ensuring no overlapping entries + */ + protected class SavedEntrySetter + extends AddressRangeMapSetter, SavedEntry> { + @Override + protected AddressRange getRange(Entry entry) { + return entry.getValue().from; + } + + @Override + protected SavedEntry getValue(Entry entry) { + return entry.getValue(); + } + + @Override + protected void remove(Entry entry) { + saved.remove(entry.getKey()); + } + + @Override + protected Iterable> getIntersecting(Address lower, + Address upper) { + return subMap(lower, upper).entrySet(); + } + + @Override + protected Entry put(AddressRange range, SavedEntry value) { + saved.put(range.getMinAddress(), value.truncate(range)); + return null; + } + } + + private final NavigableMap saved; + private final SavedEntrySetter setter = new SavedEntrySetter(); + + /** + * Construct an empty (identity) register map + */ + public SavedRegisterMap() { + this.saved = new TreeMap<>(); + } + + /** + * Copy a given register map + * + * @param saved the map to copy + */ + public SavedRegisterMap(TreeMap saved) { + this.saved = new TreeMap<>(); + } + + private NavigableMap subMap(Address lower, Address upper) { + Entry adjEnt = saved.floorEntry(lower); + if (adjEnt != null && adjEnt.getValue().contains(upper)) { + lower = adjEnt.getKey(); + } + return saved.subMap(lower, true, upper, true); + } + + static AddressRange rangeForVarnode(Varnode vn) { + return new AddressRangeImpl(vn.getAddress(), vn.getAddress().add(vn.getSize() - 1)); + } + + /** + * Map a register to a stack varnode + * + * @param from the register + * @param stackVar the stack varnode + */ + public void put(Register from, Varnode stackVar) { + put(TraceRegisterUtils.rangeForRegister(from), rangeForVarnode(stackVar)); + } + + /** + * Map the given ranges, which must have equal lengths + * + * @param from the range in register space + * @param to the range in the stack segment + */ + public void put(AddressRange from, AddressRange to) { + if (from.getLength() != to.getLength()) { + throw new IllegalArgumentException("from and to must match in length"); + } + put(from, to.getMinAddress()); + } + + /** + * Map the given range to the given address + * + * @param from the range in register space + * @param to the address in the stack segment + */ + public void put(AddressRange from, Address to) { + setter.set(from, new SavedEntry(from, to)); + } + + /** + * Copy this register map + * + * @return the copy + */ + public SavedRegisterMap fork() { + return new SavedRegisterMap(new TreeMap<>(saved)); + } + + private abstract class PieceVisitor { + public U visitVarnode(Address address, int size, U user) { + AddressRange range = new AddressRangeImpl(address, address.add(size - 1)); + SavedEntry identity = new SavedEntry(range, address); + for (SavedEntry se : subMap(range.getMinAddress(), range.getMaxAddress()).values()) { + Address prev = se.from.getMinAddress().previous(); + if (prev != null) { + SavedEntry idLeft = identity.truncateMax(prev); + if (idLeft != null) { + user = visitPiece(idLeft.to, idLeft.size(), user); + } + } + SavedEntry piece = se.intersect(range); + user = visitPiece(piece.to, piece.size(), user); + Address next = se.from.getMaxAddress().next(); + if (next == null) { + return user; + } + identity = identity.truncateMin(next); + } + if (identity != null) { + user = visitPiece(identity.to, identity.size(), user); + } + return user; + } + + abstract U visitPiece(Address address, int size, U user); + } + + /** + * Get a variable from the given state wrt. this mapping + * + *

    + * Register reads are redirected to the mapped addresses when applicable. + * + * @param the type of values in the state + * @param state the state to access + * @param address the address of the variable + * @param size the size of the variable + * @param reason a reason for reading the variable + * @return the variable's value + */ + public T getVar(PcodeExecutorState state, Address address, int size, Reason reason) { + PcodeArithmetic arithmetic = state.getArithmetic(); + return new PieceVisitor() { + @Override + T visitPiece(Address address, int sz, T value) { + T piece = state.getVar(address, size, true, reason); + return ArithmeticVarnodeEvaluator.catenate(arithmetic, size, value, piece, sz); + } + }.visitVarnode(address, size, arithmetic.fromConst(0, size)); + } + + /** + * Set a variable using the given editor wrt. this mapping + * + * @param editor the editor + * @param address the address of the variable + * @param bytes the bytes (in language-dependent endianness) giving the variable's value + * @return a future that completes when all editing commands have completed + */ + public CompletableFuture setVar(StateEditor editor, Address address, byte[] bytes) { + AsyncFence fence = new AsyncFence(); + new PieceVisitor() { + @Override + ByteBuffer visitPiece(Address address, int size, ByteBuffer buf) { + byte[] sub = new byte[size]; + buf.get(sub); + fence.include(editor.setVariable(address, sub)); + return buf; + } + }.visitVarnode(address, bytes.length, ByteBuffer.wrap(bytes)); + return fence.ready(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/StackUnwindWarning.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/StackUnwindWarning.java new file mode 100644 index 0000000000..d598483917 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/StackUnwindWarning.java @@ -0,0 +1,199 @@ +/* ### + * 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.stack; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import ghidra.program.model.address.Address; +import ghidra.program.model.data.DataType; +import ghidra.program.model.listing.Function; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.program.model.pcode.PcodeOpAST; + +/** + * A warning issued while unwinding a stack + * + *

    + * This is designed to avoid the untamed bucket of messages that a warning set usually turns into. + * In essence, it's still a bucket of messages; however, each type is curated and has some logic for + * how it interacts with other messages and additional instances of itself. + */ +public interface StackUnwindWarning { + /** + * A warning that can be combined with other instances of itself + * + * @param the same type as me (recursive) + */ + interface Combinable { + String summarize(Collection all); + } + + /** + * Get the message for display + * + * @return the message + */ + String getMessage(); + + /** + * Check if the given warning can be omitted on account of this warning + * + *

    + * Usually, the unwinder should be careful not to emit unnecessary warnings, but at times that + * can be difficult, and its proper implementation may complicate the actual unwind logic. This + * allows the unnecessary warning to be removed afterward. + * + * @param other the other warning + * @return true if this warning deems the other unnecessary + */ + default boolean moots(StackUnwindWarning other) { + return false; + } + + /** + * The unwind analyzer could not find an exit path from the frame's program counter. + */ + public record NoReturnPathStackUnwindWarning(Address pc) implements StackUnwindWarning { + @Override + public String getMessage() { + return "Could not find a path from " + pc + " to a return"; + } + + @Override + public boolean moots(StackUnwindWarning other) { + return other instanceof OpaqueReturnPathStackUnwindWarning; + } + } + + /** + * The unwind analyzer discovered at last one exit path, but none could be analyzed. + */ + public record OpaqueReturnPathStackUnwindWarning(Address pc) implements StackUnwindWarning { + @Override + public String getMessage() { + return "Could not analyze any path from " + pc + " to a return"; + } + } + + /** + * While analyzing instructions, the unwind analyzer encountered a call to a function whose + * effect on the stack is unknown. + * + *

    + * The analyzer does not descend into calls or otherwise implement inter-procedural analysis. + * Instead, it relies on analysis already performed by Ghidra's other analyzers and/or the human + * user. The analyzer will assume a reasonable default. + */ + public record UnknownPurgeStackUnwindWarning(Function function) + implements StackUnwindWarning, Combinable { + @Override + public String getMessage() { + return "Function " + function + " has unknown/invalid stack purge"; + } + + @Override + public String summarize(Collection all) { + Stream sortedDisplay = + all.stream().map(w -> w.function.getName(false)).sorted(); + if (all.size() > 7) { + return "Functions " + + sortedDisplay.limit(7).collect(Collectors.joining(", ")) + + ", ... have unknown/invalid stack purge."; + } + return "Functions " + sortedDisplay.collect(Collectors.joining(", ")) + + " have unknown/invalid stack purge."; + } + } + + /** + * While analyzing instructions, the unwind analyzer encountered a call to a function whose + * convention is not known. + * + *

    + * The analyzer will assume the default convention for the program's compiler. + */ + public record UnspecifiedConventionStackUnwindWarning(Function function) + implements StackUnwindWarning, Combinable { + @Override + public String getMessage() { + return "Function " + function + " has unspecified convention. Using default"; + } + + @Override + public String summarize(Collection all) { + Stream sortedDisplay = + all.stream().map(w -> w.function.getName(false)).sorted(); + if (all.size() > 7) { + return "Functions " + + sortedDisplay.limit(7).collect(Collectors.joining(", ")) + + ", ... have unspecified convention."; + } + return "Functions " + sortedDisplay.collect(Collectors.joining(", ")) + + " have unspecified convention."; + } + } + + /** + * While analyzing an indirect call, using the decompiler, the unwind analyzer obtained multiple + * high {@link PcodeOp#CALL} or {@link PcodeOp#CALLIND} p-code ops. + * + *

    + * Perhaps this should be replaced by an assertion, but failing fast may not be a good approach + * for this case. + */ + public record MultipleHighCallsStackUnwindWarning(List found) + implements StackUnwindWarning { + @Override + public String getMessage() { + return "Caller generated multiple decompiled calls. How?: " + found; + } + } + + /** + * Similar to {@link MultipleHighCallsStackUnwindWarning}, except no high call p-code ops. + */ + public record NoHighCallsStackUnwindWarning(PcodeOp op) implements StackUnwindWarning { + @Override + public String getMessage() { + return "Caller generated no decompiled calls. How?:" + op; + } + } + + /** + * While analyzing an indirect call, the target's type was not a function pointer. + */ + public record UnexpectedTargetTypeStackUnwindWarning(DataType type) + implements StackUnwindWarning { + @Override + public String getMessage() { + return "Indirect call target has unexpected type: " + type; + } + } + + /** + * While analyzing an indirect call, the signature could not be derived from call-site context. + */ + public record CouldNotRecoverSignatureStackUnwindWarning(PcodeOpAST op) + implements StackUnwindWarning { + @Override + public String getMessage() { + return "Could not recover signature of indirect call: " + op; + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/StackUnwindWarningSet.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/StackUnwindWarningSet.java new file mode 100644 index 0000000000..3d3a652cd7 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/StackUnwindWarningSet.java @@ -0,0 +1,160 @@ +/* ### + * 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.stack; + +import java.util.*; +import java.util.stream.Collectors; + +import ghidra.app.plugin.core.debug.stack.StackUnwindWarning.Combinable; + +/** + * A bucket of warnings + * + *

    + * This collects stack unwind warnings and then culls, and combines them for display. + */ +public class StackUnwindWarningSet implements Collection { + private final Collection warnings = new LinkedHashSet<>(); + + /** + * Create a new empty set + */ + public StackUnwindWarningSet() { + } + + /** + * Create a new set with the given initial warnings + * + * @param warnings the warnings + */ + public StackUnwindWarningSet(StackUnwindWarning... warnings) { + this.warnings.addAll(Arrays.asList(warnings)); + } + + /** + * Copy the given set + * + * @param warnings the other set + */ + public StackUnwindWarningSet(Collection warnings) { + this.warnings.addAll(warnings); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof StackUnwindWarningSet that)) { + return false; + } + return this.warnings.equals(that.warnings); + } + + @Override + public int size() { + return warnings.size(); + } + + @Override + public boolean isEmpty() { + return warnings.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return warnings.contains(o); + } + + @Override + public Iterator iterator() { + return warnings.iterator(); + } + + @Override + public Object[] toArray() { + return warnings.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return warnings.toArray(a); + } + + @Override + public boolean add(StackUnwindWarning e) { + return warnings.add(e); + } + + @Override + public boolean remove(Object o) { + return warnings.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + return warnings.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + return warnings.addAll(c); + } + + @Override + public boolean removeAll(Collection c) { + return warnings.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return warnings.retainAll(c); + } + + @Override + public void clear() { + warnings.clear(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public String summarize() { + Set> combined = new LinkedHashSet<>(); + List lines = new ArrayList<>(); + for (StackUnwindWarning w : warnings) { + if (warnings.stream().anyMatch(mw -> mw.moots(w))) { + // do nothing + } + else if (w instanceof Combinable c) { + Class cls = w.getClass(); + if (!combined.add(cls)) { + continue; + } + Collection all = + warnings.stream().filter(cw -> cls.isInstance(cw)).collect(Collectors.toList()); + if (all.size() == 1) { + lines.add(w.getMessage()); + } + else { + lines.add(c.summarize(all)); + } + } + else { + lines.add(w.getMessage()); + } + } + return lines.stream().collect(Collectors.joining("\n")); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/StackUnwinder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/StackUnwinder.java new file mode 100644 index 0000000000..ccfc5a81b2 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/StackUnwinder.java @@ -0,0 +1,316 @@ +/* ### + * 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.stack; + +import java.util.*; + +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.framework.plugintool.PluginTool; +import ghidra.pcode.exec.DebuggerPcodeUtils; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValue; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValuePcodeExecutorState; +import ghidra.pcode.exec.PcodeExecutorState; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.data.CategoryPath; +import ghidra.program.model.lang.CompilerSpec; +import ghidra.program.model.lang.Register; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.Reference; +import ghidra.program.util.ProgramLocation; +import ghidra.trace.model.*; +import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.memory.TraceMemoryState; +import ghidra.trace.model.stack.TraceStack; +import ghidra.trace.model.stack.TraceStackFrame; +import ghidra.trace.model.thread.TraceThread; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A mechanism for unwinding the stack or parts of it + * + *

    + * It can start at any frame for which the program counter and stack pointer are known. The choice + * of starting frame is informed by some tradeoffs. For making sense of a specific frame, it might + * be best to start at the nearest frame with confidently recorded PC and SP values. This will + * ensure there is little room for error unwinding from the known frame to the desired frame. For + * retrieving variable values, esp. variables stored in registers, it might be best to start at the + * innermost frame, unless all registers in a nearer frame are confidently recorded. The registers + * in frame 0 are typically recorded with highest confidence. This will ensure that all saved + * register values are properly restored from the stack into the desired frame. + * + *

    + * The usage pattern is typically: + * + *

    + * StackUnwinder unwinder = new StackUnwinder(tool, coordinates.getPlatform());
    + * for (AnalysisUnwoundFrame frame : unwinder.frames(coordinates.frame(0), monitor)) {
    + * 	// check and/or cache the frame
    + * }
    + * 
    + * + *

    + * Typically, a frame is sought either by its level or by its function. Once found, several + * operations can be performed with it, including applying annotations to the listing for the stack + * segment (see {@link AnalysisUnwoundFrame#applyToListing(int, TaskMonitor)}) and computing values + * of variables (see {@link UnwoundFrame}.) The iterator unwinds each frame lazily. If the iterator + * stops sooner than expected, consider using {@link #start(DebuggerCoordinates, TaskMonitor)} and + * {@link AnalysisUnwoundFrame#unwindNext(TaskMonitor)} directly to get better diagnostics. + */ +public class StackUnwinder { + public static final CategoryPath FRAMES_PATH = new CategoryPath("/Frames"); + public static final int PC_OP_INDEX = Reference.MNEMONIC; + public static final int BASE_OP_INDEX = 0; + + private static DebuggerStaticMappingService getMappings(PluginTool tool) { + return tool.getService(DebuggerStaticMappingService.class); + } + + private static WatchValuePcodeExecutorState getState(PluginTool tool, + DebuggerCoordinates coordinates) { + return DebuggerPcodeUtils.buildWatchState(tool, coordinates); + } + + private final PluginTool tool; + private final DebuggerStaticMappingService mappings; + final TracePlatform platform; + final Trace trace; + + final Register pc; + final AddressSpace codeSpace; + private final Register sp; + private final AddressSpace stackSpace; + + /** + * Construct an unwinder + * + * @param tool the tool with applicable modules opened as programs + * @param platform the trace platform (for registers, spaces, and stack conventions) + */ + public StackUnwinder(PluginTool tool, TracePlatform platform) { + this.tool = tool; + this.mappings = getMappings(tool); + this.platform = platform; + this.trace = platform.getTrace(); + + this.pc = Objects.requireNonNull(platform.getLanguage().getProgramCounter(), + "Platform must have a program counter"); + this.codeSpace = platform.getLanguage().getDefaultSpace(); + + CompilerSpec compiler = platform.getCompilerSpec(); + this.sp = Objects.requireNonNull(compiler.getStackPointer(), + "Platform must have a stack pointer"); + this.stackSpace = compiler.getStackBaseSpace(); + } + + /** + * Begin unwinding frames that can evaluate variables as {@link WatchValue}s + * + *

    + * While the returned frame is not technically "unwound," it is necessary to derive its base + * pointer in order to evaluate any of its variables and unwind subsequent frames. The returned + * frame has the {@link AnalysisUnwoundFrame#unwindNext(TaskMonitor)} method. + * + * @param coordinates the starting coordinates, particularly the frame level + * @param monitor a monitor for cancellation + * @return the frame for the given level + * @throws CancelledException if the monitor is cancelled + */ + public AnalysisUnwoundFrame start(DebuggerCoordinates coordinates, + TaskMonitor monitor) + throws CancelledException { + if (coordinates.getPlatform() != platform) { + throw new IllegalArgumentException("Not same platform"); + } + return start(coordinates, getState(tool, coordinates), monitor); + } + + /** + * Begin unwinding frames that can evaluate variables from the given state + * + *

    + * If is the caller's responsibility to ensure that the given state corresponds to the given + * coordinates. If they do not, the result is undefined. + * + *

    + * The starting frame's program counter and stack pointer are derived from the trace (in + * coordinates), not the state. The program counter will be retrieved from the + * {@link TraceStackFrame} if available. Otherwise, it will use the value in the register bank + * for the starting frame level. If it is not known, the unwind fails. The static (module) + * mappings are used to find the function containing the program counter, and that function is + * analyzed for its unwind info, wrt. the mapped program counter. See + * {@link UnwindAnalysis#computeUnwindInfo(Address, TaskMonitor)}. Depending on the complexity + * of the function, that analysis may be expensive. If the function cannot be found, the unwind + * fails. If analysis fails, the resulting frame may be incomplete, or the unwind may fail. + * Subsequent frames are handled similarly. See + * {@link AnalysisUnwoundFrame#unwindNext(TaskMonitor)}. + * + * @param the type of values in the state, and the result of variable evaluations + * @param coordinates the starting coordinates, particularly the frame level + * @param state the state, which must correspond to the given coordinates + * @param monitor a monitor for cancellation + * @return the frame for the given level + * @throws CancelledException if the monitor is cancelled + */ + public AnalysisUnwoundFrame start(DebuggerCoordinates coordinates, + PcodeExecutorState state, TaskMonitor monitor) throws CancelledException { + return start(coordinates, coordinates.getFrame(), state, monitor); + } + + protected AnalysisUnwoundFrame start(DebuggerCoordinates coordinates, int level, + PcodeExecutorState state, TaskMonitor monitor) throws CancelledException { + Address pcVal = null; + TraceThread thread = coordinates.getThread(); + long viewSnap = coordinates.getViewSnap(); + TraceStack stack = trace.getStackManager().getLatestStack(thread, viewSnap); + if (stack != null) { + TraceStackFrame frame = stack.getFrame(level, false); + if (frame != null) { + pcVal = frame.getProgramCounter(viewSnap); + } + } + TraceMemorySpace regs = Objects.requireNonNull( + trace.getMemoryManager().getMemoryRegisterSpace(thread, level, false), + "Frame must have a register bank"); + if (pcVal == null) { + if (TraceMemoryState.KNOWN != regs.getState(platform, viewSnap, pc)) { + throw new UnwindException("Frame must have KNOWN " + pc + " value"); + } + pcVal = codeSpace.getAddress( + regs.getValue(platform, viewSnap, pc).getUnsignedValue().longValue()); + } + if (TraceMemoryState.KNOWN != regs.getState(platform, viewSnap, sp)) { + throw new UnwindException("Frame must have KNOWN " + sp + " value"); + } + Address spVal = stackSpace.getAddress( + regs.getValue(platform, viewSnap, sp).getUnsignedValue().longValue()); + return unwind(coordinates, level, pcVal, spVal, state, new SavedRegisterMap(), monitor); + } + + /** + * Compute the unwind information for the given program counter and context + * + *

    + * For the most part, this just translates the dynamic program counter to a static program + * address and then invokes {@link UnwindAnalysis#computeUnwindInfo(Address, TaskMonitor)}. + * + * @param snap the snapshot key (used for mapping the program counter to a program database) + * @param level the frame level, used only for error messages + * @param pcVal the program counter (dynamic) + * @param monitor a monitor for cancellation + * @return the unwind info, possibly incomplete + * @throws CancelledException if the monitor is cancelled + */ + public UnwindInfo computeUnwindInfo(long snap, int level, Address pcVal, TaskMonitor monitor) + throws CancelledException { + // TODO: Try markup in trace first? + ProgramLocation staticPcLoc = mappings == null ? null + : mappings.getOpenMappedLocation( + new DefaultTraceLocation(trace, null, Lifespan.at(snap), pcVal)); + if (staticPcLoc == null) { + throw new UnwindException("Cannot find static program for frame " + level + " (" + + pc + "=" + pcVal + ")"); + } + Program program = staticPcLoc.getProgram(); + Address staticPc = staticPcLoc.getAddress(); + // TODO: Cache these? + UnwindAnalysis ua = new UnwindAnalysis(program); + return ua.computeUnwindInfo(staticPc, monitor); + } + + AnalysisUnwoundFrame unwind(DebuggerCoordinates coordinates, int level, Address pcVal, + Address spVal, PcodeExecutorState state, SavedRegisterMap registerMap, + TaskMonitor monitor) + throws CancelledException { + try { + UnwindInfo info = computeUnwindInfo(coordinates.getSnap(), level, pcVal, monitor); + return new AnalysisUnwoundFrame<>(tool, coordinates, this, state, level, pcVal, spVal, + info, null, registerMap); + } + catch (UnwindException e) { + return new AnalysisUnwoundFrame<>(tool, coordinates, this, state, level, pcVal, spVal, + null, e, registerMap); + } + } + + /** + * An iterable wrapper for {@link #start(DebuggerCoordinates, PcodeExecutorState, TaskMonitor)} + * and {@link AnalysisUnwoundFrame#unwindNext(TaskMonitor)} + * + * @param the type of values in the state + * @param coordinates the starting coordinates + * @param state the state + * @param monitor the monitor + * @return the iterable over unwound frames + */ + public Iterable> frames(DebuggerCoordinates coordinates, + PcodeExecutorState state, TaskMonitor monitor) { + return new Iterable<>() { + @Override + public Iterator> iterator() { + return new Iterator<>() { + AnalysisUnwoundFrame next = tryStart(); + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public AnalysisUnwoundFrame next() { + AnalysisUnwoundFrame cur = next; + next = tryNext(); + return cur; + } + + private AnalysisUnwoundFrame tryStart() { + try { + return start(coordinates, state, monitor); + } + catch (UnwindException | CancelledException e) { + return null; + } + } + + private AnalysisUnwoundFrame tryNext() { + try { + return next.unwindNext(monitor); + } + catch (NoSuchElementException | UnwindException | CancelledException e) { + return null; + } + } + }; + } + }; + } + + /** + * An iterable wrapper for {@link #start(DebuggerCoordinates, TaskMonitor)} and + * {@link AnalysisUnwoundFrame#unwindNext(TaskMonitor)} + * + * @param coordinates the starting coordinates + * @param monitor the monitor + * @return the iterable over unwound frames + */ + public Iterable> frames(DebuggerCoordinates coordinates, + TaskMonitor monitor) { + return frames(coordinates, getState(tool, coordinates), monitor); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/Sym.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/Sym.java new file mode 100644 index 0000000000..756e8d3dd2 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/Sym.java @@ -0,0 +1,300 @@ +/* ### + * 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.stack; + +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.lang.CompilerSpec; +import ghidra.program.model.lang.Register; + +/** + * A symbolic value tailored for stack unwind analysis + * + *

    + * The goals of stack unwind analysis are 1) to figure the stack depth at a particular instruction, + * 2) to figure the locations of saved registers on the stack, 3) to figure the location of the + * return address, whether in a register or on the stack, and 4) to figure the change in stack depth + * from calling the function. Not surprisingly, these are the fields of {@link UnwindInfo}. To these + * ends, symbols may have only one of the following forms: + * + *

      + *
    • An opaque value: {@link OpaqueSym}, to represent expressions too complex.
    • + *
    • A constant: {@link ConstSym}, to fold constants and use as offsets.
    • + *
    • A register: {@link RegisterSym}, to detect saved registers and to generate stack offsets
    • + *
    • A stack offset, i.e., SP + c: {@link StackOffsetSym}, to fold offsets, detect stack depth, + * and to generate stack dereferences
    • + *
    • A dereference of a stack offset, i.e., *(SP + c): {@link StackDerefSym}, to detect restored + * registers and return address location
    • + *
    + * + *

    + * The rules are fairly straightforward: + * + *

      + *
    • a:Opaque + b:Any => Opaque()
    • + *
    • a:Const + b:Const => Const(val=a.val + b.val)
    • + *
    • a:Const + b:Register(reg==SP) => Offset(offset=a.val)
    • + *
    • a:Offset: + b:Const => Offset(offset=a.offset + b.val)
    • + *
    • *a:Offset => Deref(offset=a.offset)
    • + *
    • *a:Register(reg==SP) => Deref(offset=0)
    • + *
    + * + *

    + * Some minute operations are omitted for clarity. Any other operation results in Opaque(). There is + * a small fault in that Register(reg=SP) and Offset(offset=0) represent the same thing, but with + * some extra bookkeeping, it's not too terrible. By interpreting p-code and then examining the + * symbolic machine state, simple movement of data between registers and the stack can be + * summarized. + */ +sealed interface Sym { + /** + * Get the opaque symbol + * + * @return the symbol + */ + static Sym opaque() { + return OpaqueSym.OPAQUE; + } + + /** + * Add this and another symbol with the given compiler for context + * + * @param cSpec the compiler specification + * @param in2 the second symbol + * @return the resulting symbol + */ + Sym add(CompilerSpec cSpec, Sym in2); + + /** + * Subtract another symbol from this with the given compiler for context + * + * @param cSpec the compiler specification + * @param in2 the second symbol + * @return the resulting symbol + */ + default Sym sub(CompilerSpec cSpec, Sym in2) { + return add(cSpec, in2.twosComp()); + } + + /** + * Negate this symbol + * + * @return the resulting symbol + */ + Sym twosComp(); + + /** + * Get the size of this symbol with the given compiler for context + * + * @param cSpec the compiler specification + * @return the size in bytes + */ + long sizeOf(CompilerSpec cSpec); + + /** + * Get a constant symbol + * + * @param value the value + * @return the constant (with size 8 bytes) + */ + static Sym constant(long value) { + return new ConstSym(value, 8); + } + + /** + * When this symbol is used as the offset in a given address space, translate it to the address + * if possible + * + *

    + * The address will be used by the state to retrieve the appropriate (symbolic) value, possibly + * generating a fresh symbol. If the address is {@link Address#NO_ADDRESS}, then the state will + * yield the opaque symbol. For sets, the state will store the given symbolic value at the + * address. If it is {@link Address#NO_ADDRESS}, then the value is ignored. + * + * @param space the space being dereferenced + * @param cSpec the compiler specification + * @return the address, or {@link Address#NO_ADDRESS} + */ + Address addressIn(AddressSpace space, CompilerSpec cSpec); + + /** + * The singleton opaque symbol + */ + public enum OpaqueSym implements Sym { + /** + * Singleton instance + */ + OPAQUE; + + @Override + public Sym add(CompilerSpec cSpec, Sym in2) { + return this; + } + + @Override + public Sym twosComp() { + return this; + } + + @Override + public long sizeOf(CompilerSpec cSpec) { + throw new UnsupportedOperationException(); + } + + @Override + public Address addressIn(AddressSpace space, CompilerSpec cSpec) { + return Address.NO_ADDRESS; + } + } + + /** + * A constant symbol + */ + public record ConstSym(long value, int size) implements Sym { + @Override + public Sym add(CompilerSpec cSpec, Sym in2) { + if (in2 instanceof ConstSym const2) { + return new ConstSym(value + const2.value, size); + } + if (in2 instanceof RegisterSym reg2) { + if (reg2.register() == cSpec.getStackPointer()) { + return new StackOffsetSym(value); + } + return Sym.opaque(); + } + if (in2 instanceof StackOffsetSym off2) { + return new StackOffsetSym(value + off2.offset); + } + return Sym.opaque(); + } + + @Override + public Sym twosComp() { + return new ConstSym(-value, size); + } + + @Override + public long sizeOf(CompilerSpec cSpec) { + return size; + } + + @Override + public Address addressIn(AddressSpace space, CompilerSpec cSpec) { + if (space.isConstantSpace() || space.isRegisterSpace() || space.isUniqueSpace()) { + return space.getAddress(value); + } + return Address.NO_ADDRESS; + } + } + + /** + * A register symbol + */ + public record RegisterSym(Register register) implements Sym { + @Override + public Sym add(CompilerSpec cSpec, Sym in2) { + if (in2 instanceof ConstSym const2) { + return const2.add(cSpec, this); + } + return Sym.opaque(); + } + + @Override + public Sym twosComp() { + return Sym.opaque(); + } + + @Override + public long sizeOf(CompilerSpec cSpec) { + return register.getMinimumByteSize(); + } + + @Override + public Address addressIn(AddressSpace space, CompilerSpec cSpec) { + if (register != cSpec.getStackPointer()) { + return Address.NO_ADDRESS; + } + if (space != cSpec.getStackBaseSpace()) { + return Address.NO_ADDRESS; + } + return cSpec.getStackSpace().getAddress(0); + } + } + + /** + * A stack offset symbol + * + *

    + * This represents a value in the form SP + c, where SP is the stack pointer register and c is a + * constant. + */ + public record StackOffsetSym(long offset) implements Sym { + @Override + public Sym add(CompilerSpec cSpec, Sym in2) { + if (in2 instanceof ConstSym const2) { + return new StackOffsetSym(offset + const2.value()); + } + return Sym.opaque(); + } + + @Override + public Sym twosComp() { + return Sym.opaque(); + } + + @Override + public long sizeOf(CompilerSpec cSpec) { + return cSpec.getStackPointer().getMinimumByteSize(); + } + + @Override + public Address addressIn(AddressSpace space, CompilerSpec cSpec) { + if (space != cSpec.getStackBaseSpace()) { + return Address.NO_ADDRESS; + } + return cSpec.getStackSpace().getAddress(offset); + } + } + + /** + * A stack dereference symbol + * + *

    + * This represents a dereferenced {@link StackOffsetSym} (or the dereferenced stack pointer + * register, in which is treated as a stack offset of 0). + */ + public record StackDerefSym(long offset, int size) implements Sym { + @Override + public Sym add(CompilerSpec cSpec, Sym in2) { + return Sym.opaque(); + } + + @Override + public Sym twosComp() { + return Sym.opaque(); + } + + @Override + public long sizeOf(CompilerSpec cSpec) { + return size; + } + + @Override + public Address addressIn(AddressSpace space, CompilerSpec cSpec) { + return Address.NO_ADDRESS; + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymPcodeArithmetic.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymPcodeArithmetic.java new file mode 100644 index 0000000000..5687e82e94 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymPcodeArithmetic.java @@ -0,0 +1,102 @@ +/* ### + * 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.stack; + +import ghidra.app.plugin.core.debug.stack.Sym.ConstSym; +import ghidra.pcode.exec.ConcretionError; +import ghidra.pcode.exec.PcodeArithmetic; +import ghidra.pcode.utils.Utils; +import ghidra.program.model.lang.*; +import ghidra.program.model.pcode.PcodeOp; + +/** + * The interpretation of arithmetic p-code ops in the domain of {@link Sym} for a specific compiler + * specification + */ +class SymPcodeArithmetic implements PcodeArithmetic { + + private final Language language; + private final CompilerSpec cSpec; + + /** + * Construct the arithmetic + * + * @param cSpec the compiler specification + */ + public SymPcodeArithmetic(CompilerSpec cSpec) { + this.cSpec = cSpec; + this.language = cSpec.getLanguage(); + } + + @Override + public Endian getEndian() { + return language.isBigEndian() ? Endian.BIG : Endian.LITTLE; + } + + @Override + public Sym unaryOp(int opcode, int sizeout, int sizein1, Sym in1) { + switch (opcode) { + case PcodeOp.COPY: + return in1; + default: + return Sym.opaque(); + } + } + + @Override + public Sym binaryOp(int opcode, int sizeout, int sizein1, Sym in1, int sizein2, + Sym in2) { + switch (opcode) { + case PcodeOp.INT_ADD: + return in1.add(cSpec, in2); + case PcodeOp.INT_SUB: + return in1.sub(cSpec, in2); + default: + return Sym.opaque(); + } + } + + @Override + public Sym modBeforeStore(int sizeout, int sizeinAddress, Sym inAddress, + int sizeinValue, Sym inValue) { + return inValue; + } + + @Override + public Sym modAfterLoad(int sizeout, int sizeinAddress, Sym inAddress, + int sizeinValue, Sym inValue) { + return inValue; + } + + @Override + public Sym fromConst(byte[] value) { + return new ConstSym(Utils.bytesToLong(value, value.length, language.isBigEndian()), + value.length); + } + + @Override + public byte[] toConcrete(Sym value, Purpose purpose) { + if (value instanceof ConstSym constVal) { + return Utils.longToBytes(constVal.value(), constVal.size(), language.isBigEndian()); + } + throw new ConcretionError("Not a constant: " + value, purpose); + } + + @Override + public long sizeOf(Sym value) { + return value.sizeOf(cSpec); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymPcodeExecutor.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymPcodeExecutor.java new file mode 100644 index 0000000000..61c11be009 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymPcodeExecutor.java @@ -0,0 +1,392 @@ +/* ### + * 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.stack; + +import java.util.*; + +import ghidra.app.decompiler.DecompInterface; +import ghidra.app.decompiler.DecompileResults; +import ghidra.app.plugin.core.debug.stack.StackUnwindWarning.*; +import ghidra.app.plugin.processors.sleigh.SleighException; +import ghidra.app.plugin.processors.sleigh.SleighLanguage; +import ghidra.pcode.exec.*; +import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.*; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.pcode.*; +import ghidra.util.exception.InvalidInputException; +import ghidra.util.task.TaskMonitor; + +/** + * The interpreter of p-code ops in the domain of {@link Sym} + * + *

    + * This is used for static analysis by executing specific basic blocks. As such, it should never be + * expected to interpret a conditional jump. (TODO: This rule might be violated if a fall-through + * instruction has internal conditional branches.... To fix would require breaking the p-code down + * into basic blocks.) We also do not want it to descend into subroutines. Thus, we must treat calls + * differently. Most of the implementation of this class is to attend to function calls, especially, + * indirect calls. For direct calls, it attempts to find the function in the same program (possibly + * in its import table) and derive the resulting stack effects from the database. Failing that, it + * issues warnings and makes reasonable assumptions. For indirect calls, it attempts to decompile + * the caller and examines the call site. If the target's type is known (presumably a function + * pointer), then the stack effects are derived from the signature and its calling convention. If + * not, then it examines the inputs and output (if applicable) to derive a signature and then + * figures the stack effects. In many cases, the stack adjustment is defined solely by the compiler, + * but for the {@code __stdcall} convention prominent in 32-bit x86 binaries for Windows, the input + * parameters must also be examined. + */ +class SymPcodeExecutor extends PcodeExecutor { + + /** + * Construct an executor for performing stack unwind analysis of a given program + * + * @param program the program to analyze + * @param state the symbolic state + * @param reason a reason to give when reading state + * @param warnings a place to emit warnings + * @param monitor a monitor for analysis, usually decompilation + * @return the executor + */ + public static SymPcodeExecutor forProgram(Program program, SymPcodeExecutorState state, + Reason reason, Set warnings, TaskMonitor monitor) { + CompilerSpec cSpec = program.getCompilerSpec(); + SleighLanguage language = (SleighLanguage) cSpec.getLanguage(); + SymPcodeArithmetic arithmetic = new SymPcodeArithmetic(cSpec); + return new SymPcodeExecutor(program, cSpec, language, arithmetic, state, reason, + warnings, monitor); + } + + private final Program program; + private final Register sp; + private final Set warnings; + private final TaskMonitor monitor; + + private final DecompInterface decomp = new DecompInterface(); + // TODO: This could perhaps be moved into AnalysisForPC? + // Meh, as it is, it should only have at most 1 entry + private final Map decompCache = new HashMap<>(); + + public SymPcodeExecutor(Program program, CompilerSpec cSpec, SleighLanguage language, + SymPcodeArithmetic arithmetic, SymPcodeExecutorState state, Reason reason, + Set warnings, TaskMonitor monitor) { + super(language, arithmetic, state, reason); + this.program = program; + this.sp = cSpec.getStackPointer(); + this.warnings = warnings; + this.monitor = monitor; + } + + @Override + public void executeCallother(PcodeOp op, PcodeFrame frame, + PcodeUseropLibrary library) { + // Do nothing + // TODO: Is there a way to know if a userop affects the stack? + } + + /** + * Attempt to figure the stack depth change for a given function + * + * @param function the function whose depth change to compute + * @param warnings a place to emit warnings + * @return the depth change, i.e., change to SP + */ + public static int computeStackChange(Function function, Set warnings) { + // TODO: How does this work for varargs functions with stack parameters? + // Seems not much heed is given to signature or call site + // Analyzers set stackPurgeSize, but still, that's on the function, not the site. + // NOTE: It seems stdcall doesn't support varargs, so this issue should not arise. + PrototypeModel convention = function.getCallingConvention(); + if (convention == null) { + if (warnings != null) { + warnings.add(new UnspecifiedConventionStackUnwindWarning(function)); + } + convention = function.getProgram().getCompilerSpec().getDefaultCallingConvention(); + } + int extrapop = convention.getExtrapop(); + if (extrapop == PrototypeModel.UNKNOWN_EXTRAPOP) { + throw new PcodeExecutionException( + "Cannot get stack change for function " + function); + } + if (function.isStackPurgeSizeValid()) { + return extrapop + function.getStackPurgeSize(); + } + if (warnings != null) { + warnings.add(new UnknownPurgeStackUnwindWarning(function)); + } + return extrapop; + } + + /** + * Attempt to figure the stack depth change for a given function + * + * @param callee the function being called + * @return the depth change, i.e., change to SP + */ + public int computeStackChange(Function callee) { + return computeStackChange(callee, warnings); + } + + @Override + public void executeCall(PcodeOp op, PcodeFrame frame, PcodeUseropLibrary library) { + Address target = op.getInput(0).getAddress(); + Function callee = program.getFunctionManager().getFunctionAt(target); + if (callee == null) { + throw new PcodeExecutionException("Callee at " + target + " is not a function.", frame); + } + String fixupName = callee.getCallFixup(); + if (fixupName != null && !"".equals(fixupName)) { + PcodeProgram snippet = + PcodeProgram.fromInject(program, fixupName, InjectPayload.CALLFIXUP_TYPE); + execute(snippet, library); + return; + } + int change = computeStackChange(callee); + adjustStack(change); + } + + /** + * Decompile the given low p-code op to its high p-code op + * + *

    + * Note this is not decompilation of the op in isolation. Decompilation usually requires a + * complete function for context. This will decompile the full containing function then examine + * the resulting high p-code ops at the same address as the given op, which are presumably those + * derived from it. It then seeks a unique call (or call indirect) op. + * + * @param op the low p-code op + * @return the high p-code op + */ + protected PcodeOpAST getHighCallOp(PcodeOp op) { + Address callSite = op.getSeqnum().getTarget(); + Function caller = program.getFunctionManager().getFunctionContaining(callSite); + + HighFunction hfunc = decompCache.computeIfAbsent(caller, c -> { + decomp.openProgram(program); + DecompileResults results = decomp.decompileFunction(c, 3, monitor); + return results.getHighFunction(); + }); + + List found = new ArrayList<>(); + Iterator oit = hfunc.getPcodeOps(callSite); + while (oit.hasNext()) { + PcodeOpAST hop = oit.next(); + if (hop.getOpcode() == PcodeOp.CALLIND || hop.getOpcode() == PcodeOp.CALL) { + found.add(hop); + } + } + if (found.size() == 1) { + return found.get(0); + } + if (found.size() > 1) { + warnings.add(new MultipleHighCallsStackUnwindWarning(found)); + return found.get(0); + } + warnings.add(new NoHighCallsStackUnwindWarning(op)); + return null; + } + + /** + * Derive the signature from the call op's target (first input) type + * + * @param op the call or call indirect op + * @return the signature if successful, or null + */ + protected FunctionSignature getSignatureFromTargetPointerType(PcodeOpAST op) { + VarnodeAST target = (VarnodeAST) op.getInput(0); + DataType dataType = target.getHigh().getDataType(); + if (!(dataType instanceof Pointer ptrType)) { + warnings.add(new UnexpectedTargetTypeStackUnwindWarning(dataType)); + return null; + } + if (!(ptrType.getDataType() instanceof FunctionSignature sigType)) { + warnings.add(new UnexpectedTargetTypeStackUnwindWarning(dataType)); + return null; + } + return sigType; + } + + /** + * Derive the signature from the call op's parameters (second and on inputs) types + * + * @param op the call or call indirect op + * @return the signature if successful, or null + */ + protected FunctionSignature getSignatureFromContextAtCallSite(PcodeOpAST op) { + FunctionDefinitionDataType sig = new FunctionDefinitionDataType("__indirect"); + sig.setReturnType(op.getOutput().getHigh().getDataType()); + // input 0 is the target, so drop it. + int numInputs = op.getNumInputs(); + Parameter[] params = new Parameter[numInputs - 1]; + ParameterDefinition[] arguments = new ParameterDefinition[numInputs - 1]; + for (int i = 1; i < numInputs; i++) { + Varnode input = op.getInput(i); + HighVariable highVar = input.getHigh(); + try { + /** + * NOTE: Not specifying storage, since: 1) It's not germane to the function + * signature, and 2) It may require chasing use-def chains through uniques. + */ + params[i - 1] = new ParameterImpl("param_" + i, highVar.getDataType(), + /*new VariableStorage(program, input),*/ program); + } + catch (InvalidInputException e) { + throw new AssertionError(e); + } + arguments[i - 1] = new ParameterDefinitionImpl("param_" + i, + input.getHigh().getDataType(), "generated"); + } + sig.setArguments(arguments); + sig.setComment("generated"); + + // TODO: Does the decompiler communicate the inferred calling convention? + try { + PrototypeModel convention = program.getCompilerSpec().findBestCallingConvention(params); + sig.setGenericCallingConvention(convention.getGenericCallingConvention()); + } + catch (SleighException e) { + // Whatever, just leave sig at "unknown" + } + return sig; + } + + /** + * Derive the function signature for an indirect call + * + *

    + * This first examines the target's type. Failing that, it examines the parameter and return + * types at the call site. + * + * @param lowOp the low p-code op + * @return the signature if successful, or null + */ + protected FunctionSignature getSignatureOfIndirectCall(PcodeOp lowOp) { + PcodeOpAST callOp = getHighCallOp(lowOp); + if (callOp == null) { + return null; + } + FunctionSignature signature = getSignatureFromTargetPointerType(callOp); + if (signature != null) { + return signature; + } + signature = getSignatureFromContextAtCallSite(callOp); + if (signature != null) { + return signature; + } + warnings.add(new CouldNotRecoverSignatureStackUnwindWarning(callOp)); + return null; + } + + /** + * Assuming the convention represents {@code __stdcall} determine the stack depth change for the + * given signature + * + * @param convention the convention, which must represent {@code __stdcall} + * @param sig the signature + * @return the depth + */ + protected int computeStdcallExtrapop(PrototypeModel convention, FunctionSignature sig) { + ParameterDefinition[] arguments = sig.getArguments(); + DataType[] types = new DataType[arguments.length + 1]; + types[0] = sig.getReturnType(); + for (int i = 0; i < arguments.length; i++) { + types[i + 1] = arguments[0].getDataType(); + } + VariableStorage[] vsLocs = convention.getStorageLocations(program, types, false); + Address min = null; + Address max = null; // Exclusive + for (VariableStorage vs : vsLocs) { + if (vs == null) { + continue; + } + for (Varnode vn : vs.getVarnodes()) { + if (!vn.getAddress().isStackAddress()) { + continue; + } + Address vnMin = vn.getAddress(); + Address vnMax = vnMin.add(vn.getSize()); + min = min == null || vnMin.compareTo(min) < 0 ? vnMin : min; + max = max == null || vnMax.compareTo(max) > 0 ? vnMax : max; + } + } + int purge = max == null ? 0 : (int) max.subtract(min); + // AFAIK, this stdcall only applies to x86, so presume return address on stack + return purge + program.getLanguage().getProgramCounter().getNumBytes(); + } + + /** + * Compute the stack change for an indirect call + * + * @param op the low p-code op + * @return the depth change + */ + protected int computeStackChangeIndirect(PcodeOp op) { + FunctionSignature sig = getSignatureOfIndirectCall(op); + if (sig == null) { + int extrapop = program.getCompilerSpec().getDefaultCallingConvention().getExtrapop(); + if (extrapop != PrototypeModel.UNKNOWN_EXTRAPOP) { + return extrapop; + } + throw new PcodeExecutionException("Cannot get stack change for indirect call: " + op); + } + PrototypeModel convention = + program.getCompilerSpec().matchConvention(sig.getGenericCallingConvention()); + if (convention == null) { + warnings.add(new UnspecifiedConventionStackUnwindWarning(null)); + convention = program.getCompilerSpec().getDefaultCallingConvention(); + } + int extrapop = convention.getExtrapop(); + if (extrapop != PrototypeModel.UNKNOWN_EXTRAPOP) { + return extrapop; + } + return computeStdcallExtrapop(convention, sig); + } + + /** + * Apply the given stack change to the machine state + * + *

    + * The overall effect is simply: {@code SP = SP + change} + * + * @param change the change + */ + protected void adjustStack(int change) { + Sym spVal = state.getVar(sp, reason); + int size = sp.getNumBytes(); + Sym spChanged = arithmetic.binaryOp(PcodeOp.INT_ADD, size, size, spVal, size, + arithmetic.fromConst(change, size)); + state.setVar(sp, spChanged); + } + + @Override + public void executeIndirectCall(PcodeOp op, PcodeFrame frame) { + int change = computeStackChangeIndirect(op); + assert change != PrototypeModel.UNKNOWN_EXTRAPOP; + adjustStack(change); + } + + @Override + public void executeConditionalBranch(PcodeOp op, PcodeFrame frame) { + // This should always end a basic block, so just do nothing + } + + @Override + protected void doExecuteIndirectBranch(PcodeOp op, PcodeFrame frame) { + // This should always end a basic block, so just do nothing + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymPcodeExecutorState.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymPcodeExecutorState.java new file mode 100644 index 0000000000..589085dbb0 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymPcodeExecutorState.java @@ -0,0 +1,282 @@ +/* ### + * 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.stack; + +import java.util.HashMap; +import java.util.Map; + +import ghidra.app.plugin.core.debug.stack.Sym.*; +import ghidra.app.plugin.core.debug.stack.SymStateSpace.SymEntry; +import ghidra.pcode.exec.PcodeArithmetic; +import ghidra.pcode.exec.PcodeArithmetic.Purpose; +import ghidra.pcode.exec.PcodeExecutorState; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemBuffer; +import ghidra.program.model.mem.MemoryBufferImpl; +import ghidra.util.Msg; + +/** + * A symbolic state for stack unwind analysis + * + *

    + * This state can store symbols in stack, register, and unique spaces. It ignores physical memory, + * since that is not typically used as temporary storage when moving values between registers and + * stack. When an address is read that does not have an entry, the state will generate a fresh + * symbol representing that address, if applicable. + */ +public class SymPcodeExecutorState implements PcodeExecutorState { + private final Program program; + private final CompilerSpec cSpec; + private final Language language; + private final SymPcodeArithmetic arithmetic; + + private final SymStateSpace stackSpace; + private final SymStateSpace registerSpace; + private final SymStateSpace uniqueSpace; + + /** + * Construct a new state for the given program + */ + public SymPcodeExecutorState(Program program) { + this.program = program; + this.cSpec = program.getCompilerSpec(); + this.language = cSpec.getLanguage(); + this.arithmetic = new SymPcodeArithmetic(cSpec); + this.stackSpace = new SymStateSpace(); + this.registerSpace = new SymStateSpace(); + this.uniqueSpace = new SymStateSpace(); + } + + protected SymPcodeExecutorState(Program program, SymPcodeArithmetic arithmetic, + SymStateSpace stackSpace, SymStateSpace registerSpace, SymStateSpace uniqueSpace) { + this.program = program; + this.cSpec = program.getCompilerSpec(); + this.language = cSpec.getLanguage(); + this.arithmetic = new SymPcodeArithmetic(cSpec); + this.stackSpace = stackSpace; + this.registerSpace = registerSpace; + this.uniqueSpace = uniqueSpace; + } + + @Override + public String toString() { + return String.format(""" + %s[ + cSpec=%s + stack=%s + registers=%s + unique=%s + ] + """, getClass().getSimpleName(), + cSpec.toString(), + stackSpace.toString(" ", language), + registerSpace.toString(" ", language), + uniqueSpace.toString(" ", language)); + } + + @Override + public Language getLanguage() { + return language; + } + + @Override + public PcodeArithmetic getArithmetic() { + return arithmetic; + } + + @Override + public void setVar(AddressSpace space, Sym offset, int size, boolean quantize, + Sym val) { + Address address = offset.addressIn(space, cSpec); + if (address.isRegisterAddress()) { + registerSpace.set(address, size, val); + } + else if (address.isUniqueAddress()) { + uniqueSpace.set(address, size, val); + } + else if (address.isConstantAddress()) { + throw new IllegalArgumentException(); + } + else if (address.isStackAddress()) { + stackSpace.set(address, size, val); + } + else { + Msg.trace(this, "Ignoring set: space=" + space + ",offset=" + offset + ",size=" + size + + ",val=" + val); + } + } + + @Override + public Sym getVar(AddressSpace space, Sym offset, int size, boolean quantize, + Reason reason) { + Address address = offset.addressIn(space, cSpec); + if (address.isRegisterAddress()) { + return registerSpace.get(address, size, arithmetic, language); + } + else if (address.isUniqueAddress()) { + return uniqueSpace.get(address, size, arithmetic, language); + } + else if (address.isConstantAddress()) { + return offset; + } + else if (address.isStackAddress()) { + return stackSpace.get(address, size, arithmetic, language); + } + return Sym.opaque(); + } + + @Override + public Map getRegisterValues() { + return Map.of(); + } + + @Override + public MemBuffer getConcreteBuffer(Address address, Purpose purpose) { + return new MemoryBufferImpl(program.getMemory(), address); + } + + @Override + public void clear() { + registerSpace.clear(); + stackSpace.clear(); + } + + @Override + public SymPcodeExecutorState fork() { + return new SymPcodeExecutorState(program, arithmetic, stackSpace.fork(), + registerSpace.fork(), uniqueSpace.fork()); + } + + /** + * Create a new state whose registers are forked from those of this state + */ + public SymPcodeExecutorState forkRegs() { + return new SymPcodeExecutorState(program, arithmetic, new SymStateSpace(), + registerSpace.fork(), new SymStateSpace()); + } + + public void dump() { + System.err.println("Registers: "); + registerSpace.dump(" ", language); + System.err.println("Unique: "); + uniqueSpace.dump(" ", language); + System.err.println("Stack: "); + stackSpace.dump(" ", language); + } + + /** + * Examine this state's SP for the overall change in stack depth + * + *

    + * There are two cases: + *

      + *
    • SP:Register(reg==SP) => depth is 0
    • + *
    • SP:Offset => depth is SP.offset
    • + *
    + * + *

    + * If SP has any other form, the depth is unknown + * + * @return the depth, or null if not known + */ + public Long computeStackDepth() { + Register sp = cSpec.getStackPointer(); + Sym expr = getVar(sp, Reason.INSPECT); + if (expr instanceof RegisterSym regVar && regVar.register() == sp) { + return 0L; + } + if (expr instanceof StackOffsetSym stackOff) { + return stackOff.offset(); + } + return null; + } + + /** + * Examine this state's PC for the location of the return address + * + *

    + * There are two cases: + *

      + *
    • PC:Register => location is PC.reg.address + *
    • PC:Deref => location is [Stack]:PC.offset + *
    + * + * @return + */ + public Address computeAddressOfReturn() { + Sym expr = getVar(language.getProgramCounter(), Reason.INSPECT); + if (expr instanceof StackDerefSym stackVar) { + return cSpec.getStackSpace().getAddress(stackVar.offset()); + } + if (expr instanceof RegisterSym regVar) { + return regVar.register().getAddress(); + } + return null; + } + + /** + * Compute a map of (saved) registers + * + *

    + * Any entry of the form (addr, v:Register) is collected as (v.register, addr). Note that the + * size of the stack entry is implied by the size of the register. + * + * @return the map from register to address + */ + public Map computeMapUsingStack() { + Map result = new HashMap<>(); + for (SymEntry ent : stackSpace.map.values()) { + if (ent.isTruncated()) { + continue; + } + if (!(ent.sym() instanceof RegisterSym regVar)) { + continue; + } + result.put(regVar.register(), ent.entRange().getMinAddress()); + } + return result; + } + + /** + * Compute the map of (restored) registers + * + *

    + * Any entry of the form (reg, v:Deref) is collected as (reg, [Stack]:v.offset). Note that the + * size of the stack entry is implied by the size of the register. + * + * @return + */ + public Map computeMapUsingRegisters() { + Map result = new HashMap<>(); + for (SymEntry ent : registerSpace.map.values()) { + if (ent.isTruncated()) { + continue; + } + if (!(ent.sym() instanceof StackDerefSym stackVar)) { + continue; + } + Register register = ent.getRegister(language); + if (register == null) { + continue; + } + result.put(register, cSpec.getStackSpace().getAddress(stackVar.offset())); + } + return result; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymStateSpace.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymStateSpace.java new file mode 100644 index 0000000000..475ba09c84 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymStateSpace.java @@ -0,0 +1,322 @@ +/* ### + * 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.stack; + +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import ghidra.app.plugin.core.debug.stack.Sym.RegisterSym; +import ghidra.app.plugin.core.debug.stack.Sym.StackDerefSym; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.trace.database.DBTraceUtils.AddressRangeMapSetter; +import ghidra.util.Msg; + +/** + * The portion of a {@link SymPcodeExecutorState} associated with a specific {@link AddressSpace}. + */ +public class SymStateSpace { + + /** + * A symbolic entry in the state + * + *

    + * It's possible the entry becomes truncated if another entry set later would overlap. Thus, it + * is necessary to remember the original range and the effective range as well as the symbol. + */ + record SymEntry(AddressRange entRange, AddressRange symRange, Sym sym) { + /** + * Create a new entry for the given range and symbol + * + * @param range the range + * @param sym the symbol + */ + SymEntry(AddressRange range, Sym sym) { + this(range, range, sym); + } + + /** + * Create a new entry for the given range and symbol + * + * @param start the min address of the range + * @param size the size in bytes of the range + * @param sym the symbol + * @throws AddressOverflowException + */ + SymEntry(Address start, int size, Sym sym) throws AddressOverflowException { + this(new AddressRangeImpl(start, size), sym); + } + + /** + * Render a human-friendly string, substituting register names for ranges where appropriate + * + * @param language optional language. If omitted, no register names are substituted + * @return the string + */ + public String toString(Language language) { + Register reg = getRegister(language); + if (reg == null) { + return toString(); + } + return String.format("%s[entRanage=%s,symRange=%s,sym=%s]", getClass().getSimpleName(), + reg, symRange, sym); + } + + /** + * Check if this entry has been truncated + * + * @return true if the effective range is equal to the original range + */ + boolean isTruncated() { + return !entRange.equals(symRange); + } + + /** + * Get the register in the language this entry's range represents + * + * @param language the language + * @return the register, or null + */ + Register getRegister(Language language) { + return language.getRegister(entRange.getMinAddress(), (int) entRange.getLength()); + } + + /** + * Create a new entry that represents a truncation of this entry + * + * @param range the subrange + * @return the new entry + */ + SymEntry truncate(AddressRange range) { + if (entRange.getMinAddress().compareTo(range.getMinAddress()) > 0) { + throw new AssertionError(); + } + if (entRange.getMaxAddress().compareTo(range.getMaxAddress()) < 0) { + throw new AssertionError(); + } + return new SymEntry(range, symRange, sym); + } + + /** + * Check if the effective range contains the given address + * + * @param address the address + * @return true if contained by this entry + */ + boolean contains(Address address) { + return entRange.contains(address); + } + + /** + * Get the symbol from this entry, applying appropriate arithmetic for truncation, if + * applicable. + * + * @param range the range to extract + * @param arithmetic the arithmetic for extracting the appropriate bytes + * @return the symbol + */ + Sym extract(AddressRange range, SymPcodeArithmetic arithmetic) { + if (symRange.equals(range)) { + return sym; + } + // TODO: Implement the extraction logic. Not sure it matters, anyway + return Sym.opaque(); + /* long shift = arithmetic.getEndian().isBigEndian() + ? symRange.getMaxAddress().subtract(range.getMaxAddress()) + : range.getMinAddress().subtract(symRange.getMinAddress()); */ + } + } + + /** + * A setter that knows how to remove or truncate overlapping entries + */ + protected class ExprMapSetter + extends AddressRangeMapSetter, SymEntry> { + @Override + protected AddressRange getRange(Entry entry) { + return entry.getValue().entRange; + } + + @Override + protected SymEntry getValue(Entry entry) { + return entry.getValue(); + } + + @Override + protected void remove(Entry entry) { + map.remove(entry.getKey()); + } + + @Override + protected Iterable> getIntersecting(Address lower, + Address upper) { + return subMap(lower, upper).entrySet(); + } + + @Override + protected Entry put(AddressRange range, SymEntry value) { + map.put(range.getMinAddress(), value.truncate(range)); + return null; + } + } + + final NavigableMap map; + private final ExprMapSetter setter = new ExprMapSetter(); + + /** + * Construct a new empty space + */ + public SymStateSpace() { + this.map = new TreeMap<>(); + } + + /** + * Construct a space with the given map (for forking) + * + * @param map the map + */ + protected SymStateSpace(NavigableMap map) { + this.map = map; + } + + @Override + public String toString() { + return toString("", null); + } + + /** + * Render a human-friendly string showing this state space + * + * @param indent the indentation + * @param language the language, optional, for register substitution + * @return the string + */ + public String toString(String indent, Language language) { + return map.values() + .stream() + .map(se -> se.toString(language)) + .collect(Collectors.joining("\n" + indent, indent + "{", "\n" + indent + "}")); + } + + private NavigableMap subMap(Address lower, Address upper) { + Entry adjEnt = map.floorEntry(lower); + if (adjEnt != null && adjEnt.getValue().contains(upper)) { + lower = adjEnt.getKey(); + } + return map.subMap(lower, true, upper, true); + } + + /** + * Set a value in this space + * + * @param address the address of the entry + * @param size the size of the entry + * @param sym the symbol + */ + public void set(Address address, int size, Sym sym) { + SymEntry entry; + try { + entry = new SymEntry(address, size, sym); + } + catch (AddressOverflowException e) { + throw new AssertionError(e); + } + setter.set(entry.entRange, entry); + } + + /** + * Get a value from this space + * + * @param address the address of the value + * @param size the size of the value + * @param arithmetic the arithmetic, in case truncation is necessary + * @param language the language, for generating symbols + * @return the symbol + */ + public Sym get(Address address, int size, SymPcodeArithmetic arithmetic, Language language) { + AddressRange range; + range = new AddressRangeImpl(address, address.add(size - 1)); + Sym result = null; + Address expectedNext = null; + for (SymEntry ent : subMap(range.getMinAddress(), range.getMaxAddress()).values()) { + if (ent.entRange.equals(range)) { + return ent.extract(range, arithmetic); + } + AddressRange intersection = range.intersect(ent.entRange); + if (expectedNext != null && !expectedNext.equals(intersection.getMinAddress())) { + return Sym.opaque(); + } + expectedNext = intersection.getMaxAddress().next(); + Sym piece = ent.extract(intersection, arithmetic); + piece = + arithmetic.unaryOp(PcodeOp.INT_ZEXT, size, (int) intersection.getLength(), piece); + if (result == null) { + result = piece; + continue; + } + result = arithmetic.binaryOp(PcodeOp.INT_OR, size, size, piece, size, result); + } + if (result != null) { + return result; + } + if (address.isRegisterAddress()) { + Register register = language.getRegister(address, size); + if (register == null) { + Msg.warn(this, "Could not figure register: address=" + address + ",size=" + size); + return Sym.opaque(); + } + return new RegisterSym(register); + } + if (address.isStackAddress()) { + return new StackDerefSym(address.getOffset(), size); + } + return Sym.opaque(); + } + + /** + * Reset this state + * + *

    + * Clears the state as if it were new. That is, it will generate fresh symbols for reads without + * existing entries. + */ + public void clear() { + map.clear(); + } + + public void dump(String prefix, Language language) { + for (SymEntry ent : map.values()) { + Register register = ent.getRegister(language); + if (register != null) { + System.err.println(prefix + register + " = " + ent.sym); + continue; + } + System.err.println(prefix + ent); + } + } + + /** + * Copy this state + * + * @return the new state + */ + public SymStateSpace fork() { + return new SymStateSpace(new TreeMap<>(map)); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindAnalysis.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindAnalysis.java new file mode 100644 index 0000000000..b9763d2e2d --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindAnalysis.java @@ -0,0 +1,516 @@ +/* ### + * 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.stack; + +import java.util.*; +import java.util.stream.Collectors; + +import generic.Unique; +import ghidra.app.plugin.core.debug.stack.StackUnwindWarning.NoReturnPathStackUnwindWarning; +import ghidra.app.plugin.core.debug.stack.StackUnwindWarning.OpaqueReturnPathStackUnwindWarning; +import ghidra.graph.*; +import ghidra.graph.algo.DijkstraShortestPathsAlgorithm; +import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; +import ghidra.pcode.exec.PcodeProgram; +import ghidra.pcode.exec.PcodeUseropLibrary; +import ghidra.program.model.address.*; +import ghidra.program.model.block.*; +import ghidra.program.model.lang.PrototypeModel; +import ghidra.program.model.lang.Register; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.FlowType; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A class for analyzing a given program's functions as a means of unwinding their stack frames in + * traces, possibly for live debug sessions. + * + * @see StackUnwinder + */ +public class UnwindAnalysis { + + /** + * A graph used for finding execution paths from function entry through the program counter to a + * return. + * + *

    + * This just wraps {@link UnwindAnalysis#blockModel} in a {@link GImplicitDirectedGraph}. + */ + class BlockGraph implements GImplicitDirectedGraph { + final TaskMonitor monitor; + + public BlockGraph(TaskMonitor monitor) { + this.monitor = monitor; + } + + List toEdgeList(CodeBlockReferenceIterator it) throws CancelledException { + List result = new ArrayList<>(); + while (it.hasNext()) { + CodeBlockReference ref = it.next(); + if (ref.getFlowType().isCall()) { + continue; + } + result.add(new BlockEdge(ref)); + } + return result; + } + + @Override + public Collection getInEdges(BlockVertex v) { + try { + return toEdgeList(blockModel.getSources(v.block, monitor)); + } + catch (CancelledException e) { + throw new AssertionError(e); + } + } + + @Override + public Collection getOutEdges(BlockVertex v) { + try { + return toEdgeList(blockModel.getDestinations(v.block, monitor)); + } + catch (CancelledException e) { + throw new AssertionError(e); + } + } + + @Override + public GDirectedGraph copy() { + throw new UnsupportedOperationException(); + } + } + + /** + * Wrap a {@link CodeBlock} + */ + record BlockVertex(CodeBlock block) { + } + + /** + * Wrap a {@link CodeBlockReference} + */ + record BlockEdge(CodeBlockReference ref) + implements GEdge { + @Override + public BlockVertex getStart() { + return new BlockVertex(ref.getSourceBlock()); + } + + @Override + public BlockVertex getEnd() { + return new BlockVertex(ref.getDestinationBlock()); + } + } + + private final Program program; + private final CodeBlockModel blockModel; + + /** + * Prepare analysis on the given program + * + * @param program the program + */ + public UnwindAnalysis(Program program) { + this.program = program; + // PartitionCodeSubModel seems to call each subroutine a block + this.blockModel = new BasicBlockModel(program); + } + + /** + * The analysis surrounding a single frame for a given program counter, i.e., instruction + * address + */ + class AnalysisForPC { + private final Address pc; + private final TaskMonitor monitor; + private final Function function; + private final BlockGraph graph; + private final BlockVertex pcBlock; + private final DijkstraShortestPathsAlgorithm pathFinder; + private final Set warnings = new HashSet<>(); + + /** + * Begin analysis for unwinding a frame, knowing only the program counter for that frame + * + *

    + * This will look up the function containing the program counter. If there's isn't one, then + * this analysis cannot proceed. + * + * @param pc the program counter + * @param monitor a monitor for progress and cancellation + * @throws CancelledException if the monitor cancels the analysis + */ + public AnalysisForPC(Address pc, TaskMonitor monitor) throws CancelledException { + this.pc = pc; + this.function = program.getFunctionManager().getFunctionContaining(pc); + if (function == null) { + throw new UnwindException("No function contains " + pc); + } + this.monitor = monitor; + this.graph = new BlockGraph(monitor); + this.pathFinder = + new DijkstraShortestPathsAlgorithm<>(graph, GEdgeWeightMetric.unitMetric()); + this.pcBlock = new BlockVertex( + Unique.assertAtMostOne(blockModel.getCodeBlocksContaining(pc, monitor))); + } + + /** + * Compute the shortest path(s) from function entry to the program counter + * + * @return the paths. There's usually only one + * @throws CancelledException if the monitor cancels the analysis + */ + public Collection> getEntryPaths() throws CancelledException { + BlockVertex entryBlock = new BlockVertex(Unique.assertAtMostOne( + blockModel.getCodeBlocksContaining(function.getEntryPoint(), monitor))); + return pathFinder.computeOptimalPaths(entryBlock, pcBlock); + } + + /** + * Find terminating blocks that return from the function + * + *

    + * If there are none, then the function is presumed non-returning. Analysis will not be + * complete. + * + *

    + * For non-returning functions, we can still use the entry path. From limited + * experimentation, it seems the extra saved-register entries are not problematic. One case + * is register parameters that the function saves to the stack for its own sake. While + * restoring those would technically be incorrect, it doesn't seem problematic to do so. + * This doesn't help us compute {@link UnwindInfo#adjust}, but that might just be + * {@link PrototypeModel#getExtrapop()}.... + * + * @return the blocks + * @throws CancelledException if the monitor cancels the analysis + */ + public Collection getReturnBlocks() + throws CancelledException { + // TODO: What to do if function is non-returning? + List returns = new ArrayList<>(); + for (CodeBlock funcBlock : blockModel.getCodeBlocksContaining(function.getBody(), + monitor)) { + FlowType flowType = funcBlock.getFlowType(); + // Omit CALL_TERMINATORs, since those are calls to non-returning functions + // TODO: This also omits tail calls by JMP + if (flowType.isTerminal() && !flowType.isCall()) { + returns.add(new BlockVertex(funcBlock)); + } + } + return returns; + } + + /** + * Compute the shortest path(s) from the program counter to a function return + * + *

    + * Because the shortest-path API does not readily permit the searching for the shortest path + * from one vertex to many vertices, we instead search for the shortest path from the + * program counter to each of the found function returns, collect all the resulting paths, + * and sort. Still, usually only the first (shortest of all) is needed. + * + * @return the paths sorted shortest first + * @throws CancelledException if the monitor cancels the analysis + */ + public Collection> getExitsPaths() throws CancelledException { + return getReturnBlocks().stream() + .flatMap(rb -> pathFinder.computeOptimalPaths(pcBlock, rb).stream()) + .sorted(Comparator.comparing(d -> d.size())) + .collect(Collectors.toList()); + } + + /** + * Execute the instructions, ordered by address, in the given address set + * + * @param exec the executor + * @param set the address set indicating the instructions to execute + * @throws CancelledException if the monitor cancels the analysis + */ + public void executeSet(SymPcodeExecutor exec, AddressSetView set) + throws CancelledException { + for (Instruction i : program.getListing().getInstructions(set, true)) { + monitor.checkCanceled(); + exec.execute(PcodeProgram.fromInstruction(i, true), PcodeUseropLibrary.nil()); + } + } + + /** + * Execute the instructions in the given block preceding the given address + * + *

    + * The instruction at {@code to} is omitted. + * + * @param exec the executor + * @param block the block whose instructions to execute + * @param to the ending address, usually the program counter + * @throws CancelledException if the monitor cancels the analysis + */ + public void executeBlockTo(SymPcodeExecutor exec, CodeBlock block, Address to) + throws CancelledException { + AddressSet set = + block.intersectRange(to.getAddressSpace().getMinAddress(), to.previous()); + executeSet(exec, set); + } + + /** + * Execute the instructions in the given block + * + * @param exec the executor + * @param block the block whose instructions to execute + * @throws CancelledException if the monitor cancels the analysis + */ + public void executeBlock(SymPcodeExecutor exec, CodeBlock block) + throws CancelledException { + executeSet(exec, block); + } + + /** + * Execute the instructions in the given block starting at the given address + * + *

    + * Instructions preceding the given address are omitted. + * + * @param exec the executor + * @param block the block whose instructions to execute + * @param from the starting address, usually the program counter + * @throws CancelledException if the monitor cancels the analysis + */ + public void executeBlockFrom(SymPcodeExecutor exec, CodeBlock block, Address from) + throws CancelledException { + AddressSet set = block.intersectRange(from, from.getAddressSpace().getMaxAddress()); + executeSet(exec, set); + } + + /** + * Execute the instructions along the given path to a destination block, omitting the final + * destination block. + * + *

    + * The given path is usually from the function entry to the block containing the program + * counter. The final block is omitted, since it should only be partially executed, i.e., + * using {@link #executeBlockTo(SymPcodeExecutor, CodeBlock, Address)}. + * + * @param exec the executor + * @param to the path to the program counter + * @see #executeToPc(Deque) + * @throws CancelledException if the monitor cancels the analysis + */ + public void executePathTo(SymPcodeExecutor exec, Deque to) + throws CancelledException { + for (BlockEdge et : to) { + executeBlock(exec, et.ref.getSourceBlock()); + } + } + + /** + * Execute the instructions along the given path from a source block, omitting the initial + * source block. + * + *

    + * The given path us usually from the block containing the program counter to a function + * return. The initial source is omitted, since it should only be partially executed, i.e., + * using {@link #executeBlockFrom(SymPcodeExecutor, CodeBlock, Address)}. + * + * @param exec the executor + * @param from the path from the program counter + * @see #executeFromPc(SymPcodeExecutorState, Deque) + * @throws CancelledException if the monitor cancels the analysis + */ + public void executePathFrom(SymPcodeExecutor exec, Deque from) + throws CancelledException { + for (BlockEdge ef : from) { + executeBlock(exec, ef.ref.getDestinationBlock()); + } + } + + /** + * Execute the instructions from entry to the program counter, using the given path + * + *

    + * This constructs a new symbolic state for stack analysis, performs the execution, and + * returns the state. The state can then be analyzed before finishing execution to a + * function return and analyzing it again. + * + * @param to the path from entry to the program counter + * @return the resulting state + * @throws CancelledException if the monitor cancels the analysis + */ + public SymPcodeExecutorState executeToPc(Deque to) throws CancelledException { + SymPcodeExecutorState state = new SymPcodeExecutorState(program); + SymPcodeExecutor exec = + SymPcodeExecutor.forProgram(program, state, Reason.EXECUTE, warnings, monitor); + executePathTo(exec, to); + executeBlockTo(exec, pcBlock.block, pc); + return state; + } + + /** + * Finish execution from the program counter to a function return, using the given path + * + *

    + * This returns the same (but mutated) state as passed to it. The state should be forked + * from the result of {@link #executeToPc(Deque)}, but resetting the stack portion. + * + * @param state the state, whose registers are forked from the result of + * {@link #executeToPc(Deque)}. + * @param from the path from the program counter to a return + * @return the resulting state + * @throws CancelledException if the monitor cancels the analysis + */ + public SymPcodeExecutorState executeFromPc(SymPcodeExecutorState state, + Deque from) throws CancelledException { + SymPcodeExecutor exec = + SymPcodeExecutor.forProgram(program, state, Reason.EXECUTE, warnings, monitor); + executeBlockFrom(exec, pcBlock.block, pc); + executePathFrom(exec, from); + return state; + } + + /** + * Compute the unwinding information for a frame presumably produced by executing the + * current function up to but excluding the program counter + * + *

    + * The goal is to compute a base pointer for the current frame so that the values of stack + * variables can be retrieved from a dynamic trace, as well as enough information to unwind + * and achieve the same for the next frame up on the stack. That is, the frame of the + * function that called the current function. We'll also need to figure out what registers + * were saved where on the stack so that the values of register variables can be retrieved + * from a dynamic trace. For architectures with a link register, register restoration is + * necessary to unwind the next frame, since that register holds its program counter. + * Ideally, this unwinding can be applied iteratively, until we reach the process' entry + * point. + * + *

    + * The analytic strategy is fairly straightforward and generalized, though not universally + * applicable. It employs a somewhat rudimentary symbolic interpretation. A symbol can be a + * constant, a register's initial value at function entry, a stack offset relative to the + * stack pointer at function entry, a dereferenced stack offset, or an opaque value. See + * {@link Sym}. + * + *

      + *
    1. Interpret the instructions along the shortest path from function entry to the program + * counter.
    2. + *
    3. Examine the symbol in the stack pointer register. It should be a stack offset. That + * offset is the "stack depth." See {@link UnwindInfo#depth()}, + * {@link UnwindInfo#computeBase(Address)}, and + * {@link SymPcodeExecutorState#computeStackDepth()}.
    4. + *
    5. Search the stack for register symbols, creating an offset-register map. A subset of + * these are the saved registers on the stack. See {@link UnwindInfo#saved} and + * {@link SymPcodeExecutorState#computeMapUsingStack()}.
    6. + *
    7. Reset the stack state. (This implies stack dereferences from further interpretation + * refer to their values at the program counter rather than function entry.) See + * {@link SymPcodeExecutorState#forkRegs()}.
    8. + *
    9. Interpret the instructions along the shortest path from the program counter to a + * function return.
    10. + *
    11. Examine the symbol in the program counter register. This gives the location (register + * or stack offset) of the return address. This strategy should work whether or not a link + * register is involved. See {@link SymPcodeExecutorState#computeAddressOfReturn()}. + *
    12. Examine the symbol in the stack pointer register, again. It should be a stack offset. + * That offset is the "stack adjustment." See {@link UnwindInfo#adjust()}, + * {@link UnwindInfo#computeNextSp(Address)}, and + * {@link SymPcodeExecutorState#computeStackDepth()}. + *
    13. Search the registers for stack dereference symbols, creating an offset-register map. + * This intersected with the same from entry to program counter is the saved registers map. + * See {@link UnwindInfo#saved()}, + * {@link UnwindInfo#mapSavedRegisters(Address, SavedRegisterMap)}, and + * {@link SymPcodeExecutorState#computeMapUsingRegisters()}. + *
    + * + *

    + * This strategy does make some assumptions: + *

      + *
    • The function returns.
    • + *
    • For every edge in the basic block graph, the stack depth at the end of its source + * block is equal to the stack depth at the start of its destination block.
    • + *
    • The function follows a "sane" convention. While it doesn't have to be any particular + * convention, it does need to restore its saved registers, and those registers should be + * saved to the stack in a straightforward manner.
    • + *
    + * + * @return the unwind information + * @throws CancelledException if the monitor cancels the analysis + */ + public UnwindInfo computeUnwindInfo() throws CancelledException { + // TODO: Find out to what other pc values this applies and cache? + Collection> entryPaths = getEntryPaths(); + if (entryPaths.isEmpty()) { + throw new UnwindException( + "Could not find a path from " + function + " entry to " + pc); + } + Collection> exitsPaths = getExitsPaths(); + // TODO: Proper exceptions for useless results + for (Deque entryPath : entryPaths) { + SymPcodeExecutorState entryState = executeToPc(entryPath); + Long depth = entryState.computeStackDepth(); + if (depth == null) { + continue; + } + if (exitsPaths.isEmpty()) { + warnings.add(new NoReturnPathStackUnwindWarning(pc)); + } + Map mapByEntry = entryState.computeMapUsingStack(); + for (Deque exitPath : exitsPaths) { + SymPcodeExecutorState exitState = + executeFromPc(entryState.forkRegs(), exitPath); + Address addressOfReturn = exitState.computeAddressOfReturn(); + Long adjust = exitState.computeStackDepth(); + if (addressOfReturn == null || adjust == null) { + continue; + } + Map mapByExit = exitState.computeMapUsingRegisters(); + mapByExit.entrySet().retainAll(mapByEntry.entrySet()); + return new UnwindInfo(function, depth, adjust, addressOfReturn, mapByExit, + new StackUnwindWarningSet(warnings)); + } + warnings.add(new OpaqueReturnPathStackUnwindWarning(pc)); + long adjust = SymPcodeExecutor.computeStackChange(function, warnings); + return new UnwindInfo(function, depth, adjust, null, mapByEntry, + new StackUnwindWarningSet(warnings)); + } + throw new UnwindException( + "Could not analyze any path from " + function + " entry to " + pc); + } + } + + /** + * Start analysis at the given program counter + * + * @param pc the program counter + * @param monitor a monitor for all the analysis that follows + * @return the pc-specific analyzer + * @throws CancelledException if the monitor cancels the analysis + */ + AnalysisForPC start(Address pc, TaskMonitor monitor) throws CancelledException { + return new AnalysisForPC(pc, monitor); + } + + /** + * Compute the unwind information for the given program counter + * + * @param pc the program counter + * @param monitor a monitor for progress and cancellation + * @return the unwind information + * @throws CancelledException if the monitor cancels the analysis + */ + public UnwindInfo computeUnwindInfo(Address pc, TaskMonitor monitor) + throws CancelledException { + AnalysisForPC analysis = start(pc, monitor); + return analysis.computeUnwindInfo(); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/ExpanderArrowExpansionListener.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindException.java similarity index 64% rename from Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/ExpanderArrowExpansionListener.java rename to Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindException.java index 9681346212..8f77ad232c 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/ExpanderArrowExpansionListener.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindException.java @@ -13,15 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package docking.widgets; +package ghidra.app.plugin.core.debug.stack; -public interface ExpanderArrowExpansionListener { - /** - * @throws ExpanderArrowExpansionVetoException - */ - default void changing(boolean expanding) throws ExpanderArrowExpansionVetoException { - // Nothing +/** + * An exception to indicate failed or incomplete stack uwinding + */ +public class UnwindException extends RuntimeException { + public UnwindException(String message) { + super(message); } - void changed(boolean expanded); + public UnwindException(String message, UnwindException cause) { + super(message, cause); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindInfo.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindInfo.java new file mode 100644 index 0000000000..64f59ddca5 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindInfo.java @@ -0,0 +1,250 @@ +/* ### + * 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.stack; + +import java.util.Map; +import java.util.Map.Entry; + +import ghidra.pcode.exec.PcodeArithmetic.Purpose; +import ghidra.pcode.exec.PcodeExecutorState; +import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.lang.Register; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Variable; +import ghidra.trace.util.TraceRegisterUtils; +import ghidra.util.task.TaskMonitor; + +/** + * Information for interpreting the current stack frame and unwinding to the next + */ +public record UnwindInfo(Function function, long depth, long adjust, Address ofReturn, + Map saved, StackUnwindWarningSet warnings) { + + /** + * The function that was analyzed + * + * @return the function + */ + public Function function() { + return function; + } + + /** + * The change in the stack pointer from function entry to the given program counter + * + *

    + * This is necessary to retrieve stack variables from the current frame. By subtracting this + * from the current stack pointer, the frame's base address is computed. See + * {@link #computeBase(Address)}. The offsets of stack variables are all relative to that base + * address. See {@link AnalysisUnwoundFrame#getValue(Variable)}. + * + * @return the depth + */ + public long depth() { + return depth; + } + + /** + * The adjustment to the stack pointer, at function entry, to return from this function + * + *

    + * This is used to unwind the stack pointer value for the next frame. + * + * @return the adjustment + */ + public long adjust() { + return adjust; + } + + /** + * The address of the return address + * + *

    + * The address may be a register or a stack offset, relative to the stack pointer at function + * entry. + * + * @return the address of the return address + */ + public Address ofReturn() { + return ofReturn; + } + + /** + * The address of the return address, given a stack base + * + *

    + * The address may be a register or a stack offset, relative to the stack pointer at function + * entry, i.e., base. If it's the latter, then this will resolve it with respect to the given + * base. The result can be used to retrieve the return address from a state. See + * {@link #computeNextPc(Address, PcodeExecutorState, Register)}. + * + * @param base the stack pointer at function entry + * @return the address of the return address + */ + public Address ofReturn(Address base) { + if (ofReturn.isRegisterAddress()) { + return ofReturn; + } + else if (ofReturn.isStackAddress()) { + return base.add(ofReturn.getOffset()); + } + throw new AssertionError(); + } + + /** + * The map of registers to stack offsets for saved registers + * + *

    + * This is not necessary until its time to unwind the next frame. The saved registers should be + * restored, then the next PC and SP computed, then the next frame unwound. See + * {@link AnalysisUnwoundFrame#unwindNext(TaskMonitor)}. + * + * @return the map of registers to stack addresses + */ + public Map saved() { + return saved; + } + + /** + * The list of warnings issues during analysis + * + * @return the warnings + */ + public StackUnwindWarningSet warnings() { + return warnings; + } + + /** + * Compute the current frame's base address given the current (or unwound) stack pointer. + * + *

    + * This is used to retrieve variable values for the current frame. + * + * @param spVal the stack pointer + * @return the base address + */ + public Address computeBase(Address spVal) { + return spVal.subtract(depth); + } + + /** + * Restore saved registers in the given state + * + *

    + * This is used as part of unwinding the next frame. + * + * @param the type of values in the state + * @param base the current frame's base pointer, as in {@link #computeBase(Address)}. + * @param state the state to modify, usually forked from the current frame's state + * @see AnalysisUnwoundFrame#unwindNext(TaskMonitor) + */ + public void restoreRegisters(Address base, PcodeExecutorState state) { + for (Entry ent : saved.entrySet()) { + Register reg = ent.getKey(); + Address offset = ent.getValue(); + assert offset.isStackAddress(); + Address address = base.add(offset.getOffset()); + T value = state.getVar(address, reg.getNumBytes(), true, Reason.INSPECT); + state.setVar(reg, value); + } + } + + /** + * Add register map entries for the saved registers in this frame + * + * @param base the current frame's base pointer, as in {@link #computeBase(Address)} + * @param registerMap the register map of the stack to this point, to be modified + */ + public void mapSavedRegisters(Address base, SavedRegisterMap map) { + for (Entry ent : saved.entrySet()) { + Register reg = ent.getKey(); + Address offset = ent.getValue(); + assert offset.isStackAddress(); + Address address = base.add(offset.getOffset()); + map.put(TraceRegisterUtils.rangeForRegister(reg), address); + } + } + + /** + * Compute the return address of the current frame, giving the unwound program counter of the + * next frame + * + *

    + * This is used as part of unwinding the next frame. + * + * @param the type of values in the state + * @param base the current frame's base pointer, as in {@link #computeBase(Address)} + * @param state the state of the next frame, whose program counter this method is computing + * @param pc the program counter register, used for its size + * @return the value of the program counter for the next frame + * @see AnalysisUnwoundFrame#unwindNext(TaskMonitor) + */ + public T computeNextPc(Address base, PcodeExecutorState state, Register pc) { + return state.getVar(ofReturn(base), pc.getNumBytes(), true, Reason.INSPECT); + } + + /** + * Compute the return address of the current frame, giving the unwound program counter (as a + * code address) of the next frame. + * + *

    + * This is used as part of unwinding the next frame. + * + * @param the type of values in the state + * @param base the current frame's base pointer, as in {@link #computeBase(Address)} + * @param state the state of the next frame, whose program counter this method is computing + * @param codeSpace the address space where the program counter points + * @param pc the program counter register, used for its size + * @return the address of the next instruction for the next frame + * @see AnalysisUnwoundFrame#unwindNext(TaskMonitor) + */ + public Address computeNextPc(Address base, PcodeExecutorState state, + AddressSpace codeSpace, Register pc) { + T value = computeNextPc(base, state, pc); + long concrete = state.getArithmetic().toLong(value, Purpose.INSPECT); + return codeSpace.getAddress(concrete); + } + + /** + * Compute the unwound stack pointer for the next frame + * + *

    + * This is used as part of unwinding the next frame. + * + * @param base the current frame's based pointer, as in {@link #computeBase(Address)} + * @return the stack pointer for the next frame + * @see AnalysisUnwoundFrame#unwindNext(TaskMonitor) + */ + public Address computeNextSp(Address base) { + return base.add(adjust); + } + + /** + * Get the number of bytes in the parameter portion of the frame + * + *

    + * These are the entries on the opposite side of the base pointer from the rest of the frame. In + * fact, these are pushed onto the stack by the caller, so these slots should be "stolen" from + * the caller's frame and given to the callee's frame. + * + * @return the total parameter size in bytes + */ + public int computeParamSize() { + return function.getStackFrame().getParameterSize(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindStackCommand.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindStackCommand.java new file mode 100644 index 0000000000..73ad2c2071 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindStackCommand.java @@ -0,0 +1,63 @@ +/* ### + * 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.stack; + +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.framework.cmd.TypedBackgroundCommand; +import ghidra.framework.plugintool.PluginTool; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValue; +import ghidra.trace.model.Trace; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A command to unwind as much of the stack as possible and annotate the resulting frame in the + * dynamic listing + */ +public class UnwindStackCommand extends TypedBackgroundCommand { + + private final PluginTool tool; + private final DebuggerCoordinates where; + + public UnwindStackCommand(PluginTool tool, DebuggerCoordinates where) { + super("Unwind Stack", false, true, false); + this.tool = tool; + this.where = where; + } + + @Override + public boolean applyToTyped(Trace obj, TaskMonitor monitor) { + try { + StackUnwinder unwinder = new StackUnwinder(tool, where.getPlatform()); + int prevParamSize = 0; + for (AnalysisUnwoundFrame frame : unwinder.frames(where.frame(0), + monitor)) { + UnwindInfo info = frame.getUnwindInfo(); + if (info != null) { + frame.applyToListing(prevParamSize, monitor); + prevParamSize = info.computeParamSize(); + } + else { + tool.setStatusInfo(frame.getError().getMessage()); + } + } + return true; + } + catch (CancelledException e) { + return true; + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwoundFrame.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwoundFrame.java new file mode 100644 index 0000000000..fe508f5183 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwoundFrame.java @@ -0,0 +1,286 @@ +/* ### + * 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.stack; + +import java.math.BigInteger; +import java.util.concurrent.CompletableFuture; + +import ghidra.app.decompiler.ClangLine; +import ghidra.app.plugin.core.debug.gui.stack.vars.VariableValueUtils; +import ghidra.app.services.DebuggerStateEditingService.StateEditor; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.lang.Register; +import ghidra.program.model.listing.*; +import ghidra.program.model.pcode.PcodeOp; + +/** + * A frame that has been unwound through analysis or annotated in the listing + * + *

    + * An unwound frame can be obtained via {@link StackUnwinder} or {@link ListingUnwoundFrame}. The + * former is used when stack unwind analysis has not yet been applied to the current trace snapshot. + * It actually returns a {@link AnalysisUnwoundFrame}, which can apply the resulting analysis to the + * snapshot. The latter is used when those annotations are already present. + * + * @param the type of values retrievable from the unwound frame + */ +public interface UnwoundFrame { + /** + * Check if this is an actual frame + * + * @see FakeUnwoundFrame + * @return true if fake + */ + boolean isFake(); + + /** + * Get the level of this frame, 0 being the innermost + * + * @return the level + */ + int getLevel(); + + /** + * Get a description of this frame, for display purposes + * + * @return the description + */ + String getDescription(); + + /** + * Get the frame's program counter + * + *

    + * If this is the innermost frame, this is the next instruction to be executed. Otherwise, this + * is the return address of the next inner frame, i.e., the instruction to be executed after + * control is returned to the function that allocated this frame. + * + * @return the frame's program counter + */ + Address getProgramCounter(); + + /** + * Get the function that allocated this frame + * + *

    + * This is the function whose body contains the program counter + * + * @return the frame's allocating function + */ + Function getFunction(); + + /** + * Get the base pointer for this frame + * + *

    + * This is the value of the stack pointer at entry of the allocating function. Note while + * related, this is a separate thing from the "base pointer" register. Not all architectures + * offer one, and even on those that do, not all functions use it. Furthermore, a function that + * does use it may place a different value in the than we define as the base pointer. The value + * here is that recovered from an examination of stack operations from the function's entry to + * the program counter. It is designed such that varnodes with stack offsets can be located in + * this frame by adding the offset to this base pointer. + * + * @return the frame's base pointer + */ + Address getBasePointer(); + + /** + * Get the frame's return address + * + *

    + * The address of the return address is determined by an examination of stack and register + * operations from the program counter to a return of the function allocating this frame. Three + * cases are known: + *

      + *
    1. The return address is on the stack. This happens for architectures where the caller must + * push the return address to the stack. It can also happen on architectures with a link + * register if the callee saves that register to the stack.
    2. + *
    3. The return address is in a register. This happens for architectures with a link register + * assuming the callee has not saved that register to the stack.
    4. + *
    5. The return address cannot be recovered. This happens when the function appears to be non + * returning, or the analysis otherwise fails to recover the return address. In this case, this + * method will throw an exception. + *
    + * + * @return the return address + */ + Address getReturnAddress(); + + /** + * Get the warnings generated during analysis + * + *

    + * Several warnings may be returned, each on its own line. + * + * @return the warnings + */ + String getWarnings(); + + /** + * Get the value of the storage from the frame + * + *

    + * Each varnode in the storage is retrieved and concatenated together. The lower-indexed + * varnodes have higher significance -- like big endian. A varnode is retrieved from the state, + * with register accesses potentially redirected to a location where its value has been saved to + * the stack. + * + *

    + * Each varnode's value is simply retrieved from the state, in contrast to + * {@link #evaluate(VariableStorage, AddressSetView)}, which ascends to varnodes' defining + * p-code ops. + * + *

    + * WARNING: Never invoke this method from the Swing thread. The state could be associated + * with a live session, and this may block to retrieve live state. + * + * @param storage the storage + * @return the value + */ + T getValue(VariableStorage storage); + + /** + * Get the value of the variable from the frame + * + *

    + * WARNING: Never invoke this method from the Swing thread. The state could be associated + * with a live session, and this may block to retrieve live state. + * + * @see #getValue(VariableStorage) + * @param variable the variable + * @return the value + */ + default T getValue(Variable variable) { + return getValue(variable.getVariableStorage()); + } + + /** + * Get the value of the register, possible saved elsewhere on the stack, relative to this frame + * + *

    + * WARNING: Never invoke this method from the Swing thread. The state could be associated + * with a live session, and this may block to retrieve live state. + * + * @param register the register + * @return the value + */ + T getValue(Register register); + + /** + * Evaluate the given storage, following defining p-code ops until symbol storage is reached + * + *

    + * This behaves similarly to {@link #getValue(VariableStorage)}, except this one will ascend + * recursively to each varnode's defining p-code op. The recursion terminates when a varnode is + * contained in the given symbol storage. The symbol storage is usually collected by examining + * the tokens on the same line, searching for ones that represent "high symbols." This ensures + * that any temporary storage used by the original program in the evaluation of, e.g., a field + * access, are not read from the current state but re-evaluated in terms of the symbols' current + * values. + * + *

    + * WARNING: Never invoke this method from the Swing thread. The state could be associated + * with a live session, and this may block to retrieve live state. + * + * @see VariableValueUtils#collectSymbolStorage(ClangLine) + * @param storage the storage to evaluate + * @param symbolStorage the terminal storage, usually that of symbols + * @return the value + */ + T evaluate(VariableStorage storage, AddressSetView symbolStorage); + + /** + * Evaluate the output for the given p-code op, ascending until symbol storage is reached + * + *

    + * WARNING: Never invoke this method from the Swing thread. The state could be associated + * with a live session, and this may block to retrieve live state. + * + * @see #evaluate(VariableStorage, AddressSetView) + * @param program the program containing the op + * @param op the op + * @param symbolStorage the terminal storage, usually that of symbols + * @return the value + */ + T evaluate(Program program, PcodeOp op, AddressSetView symbolStorage); + + /** + * Set the value of the given storage + * + *

    + * Register accesses may be redirected to the location where its current value is saved to the + * stack. + * + * @param editor the editor for setting values + * @param storage the storage to modify + * @param value the desired value + * @return a future which completes when the necessary commands have all completed + */ + CompletableFuture setValue(StateEditor editor, VariableStorage storage, BigInteger value); + + /** + * Set the value of the given variable + * + * @see #setValue(StateEditor, VariableStorage, BigInteger) + * @param editor the editor for setting values + * @param variable the variable to modify + * @param value the desired value + * @return a future which completes when the necessary commands have all completed + */ + default CompletableFuture setValue(StateEditor editor, Variable variable, + BigInteger value) { + return setValue(editor, variable.getVariableStorage(), value); + } + + /** + * Set the return address of this frame + * + *

    + * This is typically used to set up a mechanism in pure emulation that traps execution when the + * entry function has returned. For example, to emulate a target function in isolation, a script + * could load or map the target program into a trace, initialize a thread at the target + * function's entry, allocate a stack, and "unwind" that stack. Then, it can initialize the + * function's parameters and return address. The return address is usually a fake but + * recognizable address, such as {@code 0xdeadbeef}. The script would then place a breakpoint at + * that address and allow the emulator to run. Once it breaks at {@code 0xdeadbeef}, the script + * can read the return value, if applicable. + * + * @param editor the editor for setting values + * @param address the desired return address + * @return a future which completes when the necessary commands have all completed + */ + CompletableFuture setReturnAddress(StateEditor editor, Address address); + + /** + * Match length by zero extension or truncation + * + *

    + * This is to cope with a small imperfection in field expression evaluation: Fields are + * evaluated using the high p-code from the decompiled function that yielded the expression. + * That code is likely loading the value into a register, which is likely a machine word in + * size, even if the field being accessed is smaller. Thus, the type of a token's high variable + * may disagree in size with the output varnode of the token's associated high p-code op. To + * rectify this discrepancy during evaluation, the type's size is assumed correct, and the + * output value is resized to match. + * + * @param value the value + * @param length the desired length + * @return the extended or truncated value + */ + T zext(T value, int length); +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/BackgroundUtils.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/BackgroundUtils.java index 996f35ca9d..0b7db5ce97 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/BackgroundUtils.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/BackgroundUtils.java @@ -15,7 +15,7 @@ */ package ghidra.app.plugin.core.debug.utils; -import java.util.List; +import java.util.*; import java.util.concurrent.*; import java.util.function.BiFunction; import java.util.function.Function; @@ -166,20 +166,24 @@ public enum BackgroundUtils { } public static class PluginToolExecutorService extends AbstractExecutorService { - private final PluginTool tool; - private String name; - private boolean canCancel; - private boolean hasProgress; - private boolean isModal; - private final int delay; + public enum TaskOpt { + CAN_CANCEL, HAS_PROGRESS, IS_MODAL, IS_BACKGROUND; + } - public PluginToolExecutorService(PluginTool tool, String name, boolean canCancel, - boolean hasProgress, boolean isModal, int delay) { + private final PluginTool tool; + private final String name; + private final UndoableDomainObject obj; + private final int delay; + private final EnumSet opts; + + private TaskMonitor lastMonitor; + + public PluginToolExecutorService(PluginTool tool, String name, UndoableDomainObject obj, + int delay, TaskOpt... opts) { this.tool = tool; this.name = name; - this.canCancel = canCancel; - this.hasProgress = hasProgress; - this.isModal = isModal; + this.obj = obj; + this.opts = EnumSet.copyOf(Arrays.asList(opts)); this.delay = delay; } @@ -210,13 +214,45 @@ public enum BackgroundUtils { @Override public void execute(Runnable command) { - Task task = new Task(name, canCancel, hasProgress, isModal) { + if (opts.contains(TaskOpt.IS_BACKGROUND)) { + executeBackground(command); + } + else { + executeForeground(command); + } + } + + protected void executeForeground(Runnable command) { + Task task = new Task(name, + opts.contains(TaskOpt.CAN_CANCEL), + opts.contains(TaskOpt.HAS_PROGRESS), + opts.contains(TaskOpt.IS_MODAL)) { @Override public void run(TaskMonitor monitor) throws CancelledException { + lastMonitor = monitor; command.run(); } }; tool.execute(task, delay); } + + protected void executeBackground(Runnable command) { + BackgroundCommand cmd = new BackgroundCommand(name, + opts.contains(TaskOpt.HAS_PROGRESS), + opts.contains(TaskOpt.CAN_CANCEL), + opts.contains(TaskOpt.IS_MODAL)) { + @Override + public boolean applyTo(DomainObject obj, TaskMonitor monitor) { + lastMonitor = monitor; + command.run(); + return true; + } + }; + tool.executeBackgroundCommand(cmd, obj); + } + + public TaskMonitor getLastMonitor() { + return lastMonitor; + } } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/DebuggerPcodeUtils.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/DebuggerPcodeUtils.java index a2a753700c..55983a510d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/DebuggerPcodeUtils.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/pcode/exec/DebuggerPcodeUtils.java @@ -15,6 +15,13 @@ */ package ghidra.pcode.exec; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.bouncycastle.util.Arrays; + import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.service.emulation.*; import ghidra.app.plugin.core.debug.service.emulation.data.DefaultPcodeDebuggerAccess; @@ -26,13 +33,14 @@ import ghidra.pcode.exec.PcodeExecutorStatePiece.Reason; import ghidra.pcode.exec.trace.*; import ghidra.pcode.exec.trace.data.DefaultPcodeTraceAccess; import ghidra.pcode.exec.trace.data.PcodeTraceDataAccess; +import ghidra.pcode.utils.Utils; import ghidra.program.model.address.*; -import ghidra.program.model.lang.Endian; -import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.*; import ghidra.program.model.mem.MemBuffer; import ghidra.trace.model.Trace; import ghidra.trace.model.guest.TracePlatform; import ghidra.trace.model.memory.TraceMemoryState; +import ghidra.util.NumericUtilities; /** * Utilities for evaluating or executing Sleigh/p-code in the Debugger @@ -102,10 +110,160 @@ public enum DebuggerPcodeUtils { } /** - * The value of a watch expression including its state, address, and addresses read + * A wrapper on a byte array to pretty print it */ - public record WatchValue(byte[] bytes, TraceMemoryState state, Address address, + public record PrettyBytes(boolean bigEndian, byte[] bytes) { + @Override + public byte[] bytes() { + return Arrays.copyOf(bytes, bytes.length); + } + + @Override + public String toString() { + return "PrettyBytes[bigEndian=" + bigEndian + ",bytes=" + + NumericUtilities.convertBytesToString(bytes, ":") + ",value=" + + toBigInteger(false) + "]"; + } + + /** + * Render at most 256 bytes in lines of 16 space-separated bytes each + * + *

    + * If the total exceeds 256 bytes, the last line will contain ellipses and indicate the + * total size in bytes. + * + * @return the rendered string + */ + public String toBytesString() { + StringBuffer buf = new StringBuffer(); + boolean first = true; + for (int i = 0; i < bytes.length; i += 16) { + if (i >= 256) { + buf.append("\n... (count="); + buf.append(bytes.length); + buf.append(")"); + break; + } + if (first) { + first = false; + } + else { + buf.append('\n'); + } + int len = Math.min(16, bytes.length - i); + buf.append(NumericUtilities.convertBytesToString(bytes, i, len, " ")); + } + return buf.toString(); + } + + /** + * Render the bytes as an unsigned decimal integer + * + *

    + * The endianness is taken from {@link #bigEndian()} + * + * @return the rendered string + */ + public String toIntegerString() { + return toBigInteger(false).toString(); + } + + /** + * Collect various integer representations: signed, unsigned; decimal, hexadecimal + * + *

    + * This only presents those forms that differ from those already offered. The preferred form + * is unsigned decimal. If all four differ, then they are formatted on two lines: unsigned + * then signed. + * + * @return the rendered string + */ + public String collectDisplays() { + BigInteger unsigned = toBigInteger(false); + StringBuffer sb = new StringBuffer(); + String uDec = unsigned.toString(); + sb.append(uDec); + String uHex = unsigned.toString(16); + boolean radixMatters = !uHex.equals(uDec); + if (radixMatters) { + sb.append(", 0x"); + sb.append(uHex); + } + BigInteger signed = toBigInteger(true); + if (!signed.equals(unsigned)) { + sb.append(radixMatters ? "\n" : ", "); + String sDec = signed.toString(); + sb.append(sDec); + String sHex = signed.toString(16); + if (!sHex.equals(sDec)) { + sb.append(", -0x"); + sb.append(sHex.subSequence(1, sHex.length())); + } + } + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof PrettyBytes that)) { + return false; + } + if (this.bigEndian != that.bigEndian) { + return false; + } + return Arrays.areEqual(this.bytes, that.bytes); + } + + /** + * Convert the array to a big integer with the given signedness + * + * @param signed true for signed, false for unsigned + * @return the big integer + */ + public BigInteger toBigInteger(boolean signed) { + return Utils.bytesToBigInteger(bytes, bytes.length, bigEndian, signed); + } + + /** + * Get the number of bytes + * + * @return the count + */ + public int length() { + return bytes.length; + } + } + + /** + * The value of a watch expression including its state, location, and addresses read + */ + public record WatchValue(PrettyBytes bytes, TraceMemoryState state, ValueLocation location, AddressSetView reads) { + /** + * Get the value as a big integer with the given signedness + * + * @param signed true for signed, false for unsigned + * @return the big integer + */ + public BigInteger toBigInteger(boolean signed) { + return bytes.toBigInteger(signed); + } + + public Address address() { + return location == null ? null : location.getAddress(); + } + + /** + * Get the number of bytes + * + * @return the count + */ + public int length() { + return bytes.length(); + } } /** @@ -116,8 +274,8 @@ public enum DebuggerPcodeUtils { * unwieldy. */ public enum WatchValuePcodeArithmetic implements PcodeArithmetic { - BIG_ENDIAN(BytesPcodeArithmetic.BIG_ENDIAN), - LITTLE_ENDIAN(BytesPcodeArithmetic.LITTLE_ENDIAN); + BIG_ENDIAN(BytesPcodeArithmetic.BIG_ENDIAN, LocationPcodeArithmetic.BIG_ENDIAN), + LITTLE_ENDIAN(BytesPcodeArithmetic.LITTLE_ENDIAN, LocationPcodeArithmetic.LITTLE_ENDIAN); public static WatchValuePcodeArithmetic forEndian(boolean isBigEndian) { return isBigEndian ? BIG_ENDIAN : LITTLE_ENDIAN; @@ -129,15 +287,16 @@ public enum DebuggerPcodeUtils { private static final TraceMemoryStatePcodeArithmetic STATE = TraceMemoryStatePcodeArithmetic.INSTANCE; - private static final AddressOfPcodeArithmetic ADDRESS = - AddressOfPcodeArithmetic.INSTANCE; private static final AddressesReadPcodeArithmetic READS = AddressesReadPcodeArithmetic.INSTANCE; private final BytesPcodeArithmetic bytes; + private final LocationPcodeArithmetic location; - private WatchValuePcodeArithmetic(BytesPcodeArithmetic bytes) { + private WatchValuePcodeArithmetic(BytesPcodeArithmetic bytes, + LocationPcodeArithmetic location) { this.bytes = bytes; + this.location = location; } @Override @@ -148,9 +307,10 @@ public enum DebuggerPcodeUtils { @Override public WatchValue unaryOp(int opcode, int sizeout, int sizein1, WatchValue in1) { return new WatchValue( - bytes.unaryOp(opcode, sizeout, sizein1, in1.bytes), + new PrettyBytes(getEndian().isBigEndian(), + bytes.unaryOp(opcode, sizeout, sizein1, in1.bytes.bytes)), STATE.unaryOp(opcode, sizeout, sizein1, in1.state), - ADDRESS.unaryOp(opcode, sizeout, sizein1, in1.address), + location.unaryOp(opcode, sizeout, sizein1, in1.location), READS.unaryOp(opcode, sizeout, sizein1, in1.reads)); } @@ -158,9 +318,11 @@ public enum DebuggerPcodeUtils { public WatchValue binaryOp(int opcode, int sizeout, int sizein1, WatchValue in1, int sizein2, WatchValue in2) { return new WatchValue( - bytes.binaryOp(opcode, sizeout, sizein1, in1.bytes, sizein2, in2.bytes), + new PrettyBytes(getEndian().isBigEndian(), + bytes.binaryOp(opcode, sizeout, sizein1, in1.bytes.bytes, sizein2, + in2.bytes.bytes)), STATE.binaryOp(opcode, sizeout, sizein1, in1.state, sizein2, in2.state), - ADDRESS.binaryOp(opcode, sizeout, sizein1, in1.address, sizein2, in2.address), + location.binaryOp(opcode, sizeout, sizein1, in1.location, sizein2, in2.location), READS.binaryOp(opcode, sizeout, sizein1, in1.reads, sizein2, in2.reads)); } @@ -168,12 +330,13 @@ public enum DebuggerPcodeUtils { public WatchValue modBeforeStore(int sizeout, int sizeinAddress, WatchValue inAddress, int sizeinValue, WatchValue inValue) { return new WatchValue( - bytes.modBeforeStore(sizeout, sizeinAddress, inAddress.bytes, - sizeinValue, inValue.bytes), + new PrettyBytes(inValue.bytes.bigEndian, + bytes.modBeforeStore(sizeout, sizeinAddress, inAddress.bytes.bytes, + sizeinValue, inValue.bytes.bytes)), STATE.modBeforeStore(sizeout, sizeinAddress, inAddress.state, sizeinValue, inValue.state), - ADDRESS.modBeforeStore(sizeout, sizeinAddress, inAddress.address, - sizeinValue, inValue.address), + location.modBeforeStore(sizeout, sizeinAddress, inAddress.location, + sizeinValue, inValue.location), READS.modBeforeStore(sizeout, sizeinAddress, inAddress.reads, sizeinValue, inValue.reads)); } @@ -182,12 +345,13 @@ public enum DebuggerPcodeUtils { public WatchValue modAfterLoad(int sizeout, int sizeinAddress, WatchValue inAddress, int sizeinValue, WatchValue inValue) { return new WatchValue( - bytes.modAfterLoad(sizeout, sizeinAddress, inAddress.bytes, - sizeinValue, inValue.bytes), + new PrettyBytes(getEndian().isBigEndian(), + bytes.modAfterLoad(sizeout, sizeinAddress, inAddress.bytes.bytes, + sizeinValue, inValue.bytes.bytes)), STATE.modAfterLoad(sizeout, sizeinAddress, inAddress.state, sizeinValue, inValue.state), - ADDRESS.modAfterLoad(sizeout, sizeinAddress, inAddress.address, - sizeinValue, inValue.address), + location.modAfterLoad(sizeout, sizeinAddress, inAddress.location, + sizeinValue, inValue.location), READS.modAfterLoad(sizeout, sizeinAddress, inAddress.reads, sizeinValue, inValue.reads)); } @@ -195,20 +359,20 @@ public enum DebuggerPcodeUtils { @Override public WatchValue fromConst(byte[] value) { return new WatchValue( - bytes.fromConst(value), + new PrettyBytes(getEndian().isBigEndian(), bytes.fromConst(value)), STATE.fromConst(value), - ADDRESS.fromConst(value), + location.fromConst(value), READS.fromConst(value)); } @Override public byte[] toConcrete(WatchValue value, Purpose purpose) { - return bytes.toConcrete(value.bytes, purpose); + return bytes.toConcrete(value.bytes.bytes, purpose); } @Override public long sizeOf(WatchValue value) { - return bytes.sizeOf(value.bytes); + return bytes.sizeOf(value.bytes.bytes); } } @@ -216,7 +380,7 @@ public enum DebuggerPcodeUtils { implements PcodeExecutorStatePiece { private final PcodeExecutorStatePiece bytesPiece; private final PcodeExecutorStatePiece statePiece; - private final PcodeExecutorStatePiece addressPiece; + private final PcodeExecutorStatePiece locationPiece; private final PcodeExecutorStatePiece readsPiece; private final PcodeArithmetic arithmetic; @@ -224,11 +388,11 @@ public enum DebuggerPcodeUtils { public WatchValuePcodeExecutorStatePiece( PcodeExecutorStatePiece bytesPiece, PcodeExecutorStatePiece statePiece, - PcodeExecutorStatePiece addressPiece, + PcodeExecutorStatePiece locationPiece, PcodeExecutorStatePiece readsPiece) { this.bytesPiece = bytesPiece; this.statePiece = statePiece; - this.addressPiece = addressPiece; + this.locationPiece = locationPiece; this.readsPiece = readsPiece; this.arithmetic = WatchValuePcodeArithmetic.forLanguage(bytesPiece.getLanguage()); } @@ -248,12 +412,18 @@ public enum DebuggerPcodeUtils { return arithmetic; } + @Override + public WatchValuePcodeExecutorStatePiece fork() { + return new WatchValuePcodeExecutorStatePiece( + bytesPiece.fork(), statePiece.fork(), locationPiece.fork(), readsPiece.fork()); + } + @Override public void setVar(AddressSpace space, byte[] offset, int size, boolean quantize, WatchValue val) { - bytesPiece.setVar(space, offset, size, quantize, val.bytes); + bytesPiece.setVar(space, offset, size, quantize, val.bytes.bytes); statePiece.setVar(space, offset, size, quantize, val.state); - addressPiece.setVar(space, offset, size, quantize, val.address); + locationPiece.setVar(space, offset, size, quantize, val.location); readsPiece.setVar(space, offset, size, quantize, val.reads); } @@ -261,12 +431,30 @@ public enum DebuggerPcodeUtils { public WatchValue getVar(AddressSpace space, byte[] offset, int size, boolean quantize, Reason reason) { return new WatchValue( - bytesPiece.getVar(space, offset, size, quantize, reason), + new PrettyBytes(getLanguage().isBigEndian(), + bytesPiece.getVar(space, offset, size, quantize, reason)), statePiece.getVar(space, offset, size, quantize, reason), - addressPiece.getVar(space, offset, size, quantize, reason), + locationPiece.getVar(space, offset, size, quantize, reason), readsPiece.getVar(space, offset, size, quantize, reason)); } + @Override + public Map getRegisterValues() { + Map result = new HashMap<>(); + for (Entry entry : bytesPiece.getRegisterValues().entrySet()) { + Register reg = entry.getKey(); + AddressSpace space = reg.getAddressSpace(); + long offset = reg.getAddress().getOffset(); + int size = reg.getNumBytes(); + result.put(reg, new WatchValue( + new PrettyBytes(getLanguage().isBigEndian(), entry.getValue()), + statePiece.getVar(space, offset, size, false, Reason.INSPECT), + locationPiece.getVar(space, offset, size, false, Reason.INSPECT), + readsPiece.getVar(space, offset, size, false, Reason.INSPECT))); + } + return result; + } + @Override public MemBuffer getConcreteBuffer(Address address, Purpose purpose) { return bytesPiece.getConcreteBuffer(address, purpose); @@ -276,7 +464,7 @@ public enum DebuggerPcodeUtils { public void clear() { bytesPiece.clear(); statePiece.clear(); - addressPiece.clear(); + locationPiece.clear(); readsPiece.clear(); } } @@ -298,16 +486,26 @@ public enum DebuggerPcodeUtils { return piece.arithmetic; } + @Override + public WatchValuePcodeExecutorState fork() { + return new WatchValuePcodeExecutorState(piece.fork()); + } + @Override public void setVar(AddressSpace space, WatchValue offset, int size, boolean quantize, WatchValue val) { - piece.setVar(space, offset.bytes, size, quantize, val); + piece.setVar(space, offset.bytes.bytes, size, quantize, val); } @Override public WatchValue getVar(AddressSpace space, WatchValue offset, int size, boolean quantize, Reason reason) { - return piece.getVar(space, offset.bytes, size, quantize, reason); + return piece.getVar(space, offset.bytes.bytes, size, quantize, reason); + } + + @Override + public Map getRegisterValues() { + return piece.getRegisterValues(); } @Override @@ -330,7 +528,7 @@ public enum DebuggerPcodeUtils { return new WatchValuePcodeExecutorState(new WatchValuePcodeExecutorStatePiece( bytesState, new TraceMemoryStatePcodeExecutorStatePiece(data), - new AddressOfPcodeExecutorStatePiece(data.getLanguage()), + new LocationPcodeExecutorStatePiece(data.getLanguage()), new AddressesReadTracePcodeExecutorStatePiece(data))); } diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPluginScreenShots.java index 7ea69b52ff..953847f7e8 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPluginScreenShots.java @@ -15,31 +15,63 @@ */ package ghidra.app.plugin.core.debug.gui.stack; +import java.io.IOException; +import java.math.BigInteger; +import java.util.List; +import java.util.Set; + import org.junit.*; +import generic.Unique; +import ghidra.app.plugin.assembler.*; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.gui.action.SPLocationTrackingSpec; +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingProvider; +import ghidra.app.plugin.core.debug.service.editing.DebuggerStateEditingServicePlugin; +import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePlugin; +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.modules.DebuggerStaticMappingUtils; import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin; +import ghidra.app.plugin.core.debug.stack.*; import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin; import ghidra.app.services.*; +import ghidra.app.services.DebuggerEmulationService.EmulationResult; +import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; +import ghidra.app.services.DebuggerStateEditingService.StateEditor; +import ghidra.async.AsyncTestUtils; import ghidra.framework.model.DomainFolder; +import ghidra.framework.model.DomainObject; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValue; +import ghidra.program.database.ProgramDB; +import ghidra.program.disassemble.Disassembler; import ghidra.program.model.address.*; -import ghidra.program.model.listing.FunctionManager; -import ghidra.program.model.listing.Program; +import ghidra.program.model.data.UnsignedIntegerDataType; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.listing.Function.FunctionUpdateType; import ghidra.program.model.symbol.SourceType; import ghidra.program.util.ProgramLocation; import ghidra.test.ToyProgramBuilder; import ghidra.trace.database.ToyDBTraceBuilder; import ghidra.trace.model.DefaultTraceLocation; import ghidra.trace.model.Lifespan; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; import ghidra.trace.model.stack.TraceStack; import ghidra.trace.model.stack.TraceStackFrame; import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.schedule.Scheduler; +import ghidra.util.InvalidNameException; +import ghidra.util.Msg; import ghidra.util.database.UndoableTransaction; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.ConsoleTaskMonitor; import ghidra.util.task.TaskMonitor; import help.screenshot.GhidraScreenShotGenerator; -public class DebuggerStackPluginScreenShots extends GhidraScreenShotGenerator { +public class DebuggerStackPluginScreenShots extends GhidraScreenShotGenerator + implements AsyncTestUtils { ProgramManager programManager; DebuggerTraceManagerService traceManager; @@ -125,4 +157,182 @@ public class DebuggerStackPluginScreenShots extends GhidraScreenShotGenerator { captureIsolatedProvider(DebuggerStackProvider.class, 600, 300); } + + protected ConsoleTaskMonitor monitor = new ConsoleTaskMonitor(); + + // TODO: Propose this replace waitForProgram + public static void waitForDomainObject(DomainObject object) { + object.flushEvents(); + waitForSwing(); + } + + 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; + } + }); + } + + protected void createProgram(Language lang, CompilerSpec cSpec) throws IOException { + program = new ProgramDB("fibonacci", lang, cSpec, this); + } + + protected void createProgram(String languageID, String cSpecID) throws IOException { + Language language = getLanguageService().getLanguage(new LanguageID(languageID)); + CompilerSpec cSpec = cSpecID == null ? language.getDefaultCompilerSpec() + : language.getCompilerSpecByID(new CompilerSpecID(cSpecID)); + createProgram(language, cSpec); + } + + Address retInstr; + + protected Register register(String name) { + return program.getLanguage().getRegister(name); + } + + protected Function createFibonacciProgramX86_32() throws Throwable { + createProgram("x86:LE:32:default", "gcc"); + intoProject(program); + try (UndoableTransaction tid = UndoableTransaction.start(program, "Assemble")) { + Address entry = addr(program, 0x00400000); + program.getMemory() + .createInitializedBlock(".text", entry, 0x1000, (byte) 0, monitor, false); + Assembler asm = + Assemblers.getAssembler(program.getLanguage(), StackUnwinderTest.NO_16BIT_CALLS); + AssemblyBuffer buf = new AssemblyBuffer(asm, entry); + + buf.assemble("PUSH EBP"); + buf.assemble("MOV EBP, ESP"); + + buf.assemble("CMP dword ptr [EBP+8], 1"); + Address jumpBase = buf.getNext(); + buf.assemble("JBE 0x" + buf.getNext()); + + // Recursive case. Let EDX be sum + // sum = fib(n - 1) + buf.assemble("MOV ECX, dword ptr [EBP+8]"); + buf.assemble("DEC ECX"); + buf.assemble("PUSH ECX"); // pass n - 1 + buf.assemble("CALL 0x" + entry); + buf.assemble("ADD ESP, 4"); // Clear parameters + buf.assemble("MOV EDX, EAX"); + // sum += fib(n - 2) + buf.assemble("MOV ECX, dword ptr [EBP+8]"); + buf.assemble("SUB ECX, 2"); + buf.assemble("PUSH EDX"); // Caller Save EDX + buf.assemble("PUSH ECX"); // pass n - 2 + buf.assemble("CALL 0x" + entry); + buf.assemble("ADD ESP, 4"); // Clear parameters + buf.assemble("POP EDX"); // Restore EDX + buf.assemble("ADD EAX, EDX"); + + Address labelRet = buf.getNext(); + buf.assemble("LEAVE"); + retInstr = buf.getNext(); + buf.assemble("RET"); + + Address labelBase = buf.getNext(); + buf.assemble(jumpBase, "JBE 0x" + labelBase); + buf.assemble("MOV EAX, dword ptr [EBP+8]"); + buf.assemble("JMP 0x" + labelRet); + + byte[] bytes = buf.getBytes(); + program.getMemory().setBytes(entry, bytes); + + Disassembler dis = Disassembler.getDisassembler(program, monitor, null); + dis.disassemble(entry, null); + + Function function = program.getFunctionManager() + .createFunction("fib", entry, + new AddressSet(entry, entry.add(bytes.length - 1)), + SourceType.USER_DEFINED); + + function.updateFunction("__cdecl", + new ReturnParameterImpl(UnsignedIntegerDataType.dataType, program), + List.of( + new ParameterImpl("n", UnsignedIntegerDataType.dataType, program)), + FunctionUpdateType.DYNAMIC_STORAGE_FORMAL_PARAMS, true, SourceType.ANALYSIS); + // NOTE: The decompiler doesn't actually use sum.... For some reason, it re-uses n + // Still, in the tests, I can use uVar1 (EAX) as a register variable + function.addLocalVariable( + new LocalVariableImpl("sum", 0, UnsignedIntegerDataType.dataType, register("EDX"), + program), + SourceType.USER_DEFINED); + return function; + } + } + + @Test + public void testCaptureDebuggerStackUnwindInListing() throws Throwable { + addPlugin(tool, DebuggerListingPlugin.class); + + DebuggerStateEditingService editingService = + addPlugin(tool, DebuggerStateEditingServicePlugin.class); + DebuggerEmulationService emuService = addPlugin(tool, DebuggerEmulationServicePlugin.class); + + Function function = createFibonacciProgramX86_32(); + Address entry = function.getEntryPoint(); + + programManager.openProgram(program); + + tb.close(); + tb = new ToyDBTraceBuilder( + ProgramEmulationUtils.launchEmulationTrace(program, entry, this)); + tb.trace.release(this); + TraceThread thread = Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + traceManager.openTrace(tb.trace); + traceManager.activateThread(thread); + waitForSwing(); + + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + StateEditor editor = editingService.createStateEditor(tb.trace); + + DebuggerCoordinates atSetup = traceManager.getCurrent(); + StackUnwinder unwinder = new StackUnwinder(tool, atSetup.getPlatform()); + AnalysisUnwoundFrame frameAtSetup = unwinder.start(atSetup, monitor); + + Parameter param1 = function.getParameter(0); + waitOn(frameAtSetup.setValue(editor, param1, BigInteger.valueOf(9))); + waitOn(frameAtSetup.setReturnAddress(editor, tb.addr(0xdeadbeef))); + waitForTasks(); + + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getBreakpointManager() + .addBreakpoint("Breakpoints[0]", Lifespan.nowOn(0), retInstr, + Set.of(), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "unwind stack"); + } + + EmulationResult result = emuService.run(atSetup.getPlatform(), atSetup.getTime(), monitor, + Scheduler.oneThread(thread)); + Msg.debug(this, "Broke after " + result.schedule()); + + traceManager.activateTime(result.schedule()); + waitForTasks(); + DebuggerCoordinates tallest = traceManager.getCurrent(); + try (UndoableTransaction tid = tb.startTransaction()) { + new UnwindStackCommand(tool, tallest).applyTo(tb.trace, monitor); + } + waitForDomainObject(tb.trace); + + DebuggerListingProvider listingProvider = + waitForComponentProvider(DebuggerListingProvider.class); + listingProvider.setTrackingSpec(SPLocationTrackingSpec.INSTANCE); + waitForSwing(); + + captureIsolatedProvider(listingProvider, 800, 600); + } } diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueHoverPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueHoverPluginScreenShots.java new file mode 100644 index 0000000000..109fedbd0a --- /dev/null +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/stack/vars/VariableValueHoverPluginScreenShots.java @@ -0,0 +1,391 @@ +/* ### + * 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.stack.vars; + +import java.awt.Rectangle; +import java.awt.Window; +import java.awt.event.MouseEvent; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.*; + +import org.junit.Test; + +import docking.widgets.fieldpanel.FieldPanel; +import docking.widgets.fieldpanel.support.FieldLocation; +import generic.Unique; +import ghidra.app.decompiler.component.DecompilerPanel; +import ghidra.app.plugin.assembler.*; +import ghidra.app.plugin.core.codebrowser.CodeViewerProvider; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingProvider; +import ghidra.app.plugin.core.debug.service.editing.DebuggerStateEditingServicePlugin; +import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePlugin; +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.tracemgr.DebuggerTraceManagerServicePlugin; +import ghidra.app.plugin.core.debug.stack.*; +import ghidra.app.plugin.core.debug.stack.StackUnwinderTest.HoverLocation; +import ghidra.app.plugin.core.decompile.DecompilerProvider; +import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin; +import ghidra.app.services.*; +import ghidra.app.services.DebuggerEmulationService.EmulationResult; +import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; +import ghidra.app.services.DebuggerStateEditingService.StateEditor; +import ghidra.app.util.viewer.listingpanel.ListingPanel; +import ghidra.async.AsyncTestUtils; +import ghidra.framework.model.DomainFolder; +import ghidra.framework.model.DomainObject; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValue; +import ghidra.program.database.ProgramDB; +import ghidra.program.disassemble.Disassembler; +import ghidra.program.model.address.*; +import ghidra.program.model.data.UnsignedIntegerDataType; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.listing.Function.FunctionUpdateType; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.program.model.scalar.Scalar; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.SourceType; +import ghidra.program.util.GhidraProgramUtilities; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.schedule.Scheduler; +import ghidra.util.InvalidNameException; +import ghidra.util.Msg; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.ConsoleTaskMonitor; +import help.screenshot.GhidraScreenShotGenerator; + +public class VariableValueHoverPluginScreenShots extends GhidraScreenShotGenerator + implements AsyncTestUtils { + + ProgramManager programManager; + DebuggerTraceManagerService traceManager; + DebuggerStaticMappingService mappingService; + ToyDBTraceBuilder tb; + Program program; + + protected ConsoleTaskMonitor monitor = new ConsoleTaskMonitor(); + + // TODO: Propose this replace waitForProgram + public static void waitForDomainObject(DomainObject object) { + object.flushEvents(); + waitForSwing(); + } + + 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; + } + }); + } + + protected void createProgram(Language lang, CompilerSpec cSpec) throws IOException { + program = new ProgramDB("fibonacci", lang, cSpec, this); + } + + protected void createProgram(String languageID, String cSpecID) throws IOException { + Language language = getLanguageService().getLanguage(new LanguageID(languageID)); + CompilerSpec cSpec = cSpecID == null ? language.getDefaultCompilerSpec() + : language.getCompilerSpecByID(new CompilerSpecID(cSpecID)); + createProgram(language, cSpec); + } + + Map stackRefInstrs = new HashMap<>(); + Address registerRefInstr; + Address retInstr; + + protected Register register(String name) { + return program.getLanguage().getRegister(name); + } + + private static Address addr(Program program, long offset) { + return program.getAddressFactory().getDefaultAddressSpace().getAddress(offset); + } + + protected Function createFibonacciProgramX86_32() throws Throwable { + createProgram("x86:LE:32:default", "gcc"); + intoProject(program); + try (UndoableTransaction tid = UndoableTransaction.start(program, "Assemble")) { + Address entry = addr(program, 0x00400000); + program.getMemory() + .createInitializedBlock(".text", entry, 0x1000, (byte) 0, monitor, false); + Assembler asm = + Assemblers.getAssembler(program.getLanguage(), StackUnwinderTest.NO_16BIT_CALLS); + AssemblyBuffer buf = new AssemblyBuffer(asm, entry); + + buf.assemble("PUSH EBP"); + buf.assemble("MOV EBP, ESP"); + + stackRefInstrs.put(buf.getNext(), 0); + buf.assemble("CMP dword ptr [EBP+8], 1"); + Address jumpBase = buf.getNext(); + buf.assemble("JBE 0x" + buf.getNext()); + + // Recursive case. Let EDX be sum + // sum = fib(n - 1) + stackRefInstrs.put(buf.getNext(), 1); + buf.assemble("MOV ECX, dword ptr [EBP+8]"); + buf.assemble("DEC ECX"); + buf.assemble("PUSH ECX"); // pass n - 1 + buf.assemble("CALL 0x" + entry); + buf.assemble("ADD ESP, 4"); // Clear parameters + registerRefInstr = buf.getNext(); + buf.assemble("MOV EDX, EAX"); + // sum += fib(n - 2) + stackRefInstrs.put(buf.getNext(), 1); + buf.assemble("MOV ECX, dword ptr [EBP+8]"); + buf.assemble("SUB ECX, 2"); + buf.assemble("PUSH EDX"); // Caller Save EDX + buf.assemble("PUSH ECX"); // pass n - 2 + buf.assemble("CALL 0x" + entry); + buf.assemble("ADD ESP, 4"); // Clear parameters + buf.assemble("POP EDX"); // Restore EDX + buf.assemble("ADD EAX, EDX"); + + Address labelRet = buf.getNext(); + buf.assemble("LEAVE"); + retInstr = buf.getNext(); + buf.assemble("RET"); + + Address labelBase = buf.getNext(); + buf.assemble(jumpBase, "JBE 0x" + labelBase); + stackRefInstrs.put(buf.getNext(), 1); + buf.assemble("MOV EAX, dword ptr [EBP+8]"); + buf.assemble("JMP 0x" + labelRet); + + byte[] bytes = buf.getBytes(); + program.getMemory().setBytes(entry, bytes); + + Disassembler dis = Disassembler.getDisassembler(program, monitor, null); + dis.disassemble(entry, null); + + Function function = program.getFunctionManager() + .createFunction("fib", entry, + new AddressSet(entry, entry.add(bytes.length - 1)), + SourceType.USER_DEFINED); + + function.updateFunction("__cdecl", + new ReturnParameterImpl(UnsignedIntegerDataType.dataType, program), + List.of( + new ParameterImpl("n", UnsignedIntegerDataType.dataType, program)), + FunctionUpdateType.DYNAMIC_STORAGE_FORMAL_PARAMS, true, SourceType.ANALYSIS); + // NOTE: The decompiler doesn't actually use sum.... For some reason, it re-uses n + // Still, in the tests, I can use uVar1 (EAX) as a register variable + function.addLocalVariable( + new LocalVariableImpl("sum", 0, UnsignedIntegerDataType.dataType, register("EDX"), + program), + SourceType.USER_DEFINED); + + AddressSpace stack = program.getAddressFactory().getStackSpace(); + for (Map.Entry ent : stackRefInstrs.entrySet()) { + Instruction ins = program.getListing().getInstructionAt(ent.getKey()); + ins.addOperandReference(ent.getValue(), stack.getAddress(4), RefType.READ, + SourceType.ANALYSIS); + } + return function; + } + } + + protected void prepareContext() throws Throwable { + programManager = addPlugin(tool, ProgramManagerPlugin.class); + programManager.closeAllPrograms(true); + traceManager = addPlugin(tool, DebuggerTraceManagerServicePlugin.class); + mappingService = addPlugin(tool, DebuggerStaticMappingServicePlugin.class); + addPlugin(tool, DebuggerListingPlugin.class); + addPlugin(tool, VariableValueHoverPlugin.class); + + DebuggerStateEditingService editingService = + addPlugin(tool, DebuggerStateEditingServicePlugin.class); + DebuggerEmulationService emuService = addPlugin(tool, DebuggerEmulationServicePlugin.class); + + Function function = createFibonacciProgramX86_32(); + GhidraProgramUtilities.setAnalyzedFlag(program, true); + Address entry = function.getEntryPoint(); + + programManager.openProgram(program); + + tb = new ToyDBTraceBuilder( + ProgramEmulationUtils.launchEmulationTrace(program, entry, this)); + tb.trace.release(this); + TraceThread thread = Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + traceManager.openTrace(tb.trace); + traceManager.activateThread(thread); + waitForSwing(); + + try (UndoableTransaction tid = tb.startTransaction()) { + MemoryBlock block = program.getMemory().getBlock(".text"); + byte[] text = new byte[(int) block.getSize()]; + block.getBytes(block.getStart(), text); + + tb.trace.getMemoryManager().putBytes(0, block.getStart(), ByteBuffer.wrap(text)); + + Disassembler dis = + Disassembler.getDisassembler(tb.trace.getProgramView(), monitor, null); + dis.disassemble(entry, null); + } + waitForDomainObject(tb.trace); + + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + StateEditor editor = editingService.createStateEditor(tb.trace); + + DebuggerCoordinates atSetup = traceManager.getCurrent(); + StackUnwinder unwinder = new StackUnwinder(tool, atSetup.getPlatform()); + AnalysisUnwoundFrame frameAtSetup = unwinder.start(atSetup, monitor); + + Parameter param1 = function.getParameter(0); + waitOn(frameAtSetup.setValue(editor, param1, BigInteger.valueOf(9))); + waitOn(frameAtSetup.setReturnAddress(editor, tb.addr(0xdeadbeef))); + waitForTasks(); + + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getBreakpointManager() + .addBreakpoint("Breakpoints[0]", Lifespan.nowOn(0), retInstr, + Set.of(), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "unwind stack"); + } + + EmulationResult result = emuService.run(atSetup.getPlatform(), atSetup.getTime(), monitor, + Scheduler.oneThread(thread)); + Msg.debug(this, "Broke after " + result.schedule()); + + traceManager.activateTime(result.schedule()); + waitForTasks(); + DebuggerCoordinates tallest = traceManager.getCurrent(); + try (UndoableTransaction tid = tb.startTransaction()) { + new UnwindStackCommand(tool, tallest).applyTo(tb.trace, monitor); + } + waitForDomainObject(tb.trace); + } + + @Test + public void testCaptureVariableValueHoverPluginListing() throws Throwable { + prepareContext(); + + // We're cheating the address game, but both use the same language without relocation + Instruction ins = tb.trace.getCodeManager().instructions().getAt(0, registerRefInstr); + + DebuggerListingProvider listingProvider = + waitForComponentProvider(DebuggerListingProvider.class); + ListingPanel listingPanel = listingProvider.getListingPanel(); + + Window window = moveProviderToItsOwnWindow(listingProvider, 900, 600); + window.toFront(); + waitForSwing(); + + HoverLocation loc = + StackUnwinderTest.findOperandLocation(listingPanel, ins, register("EDX")); + FieldLocation fLoc = loc.fLoc(); + BigInteger refIndex = listingPanel.getAddressIndexMap().getIndex(registerRefInstr); + + FieldPanel fieldPanel = listingPanel.getFieldPanel(); + runSwing(() -> fieldPanel.goTo(refIndex, fLoc.fieldNum, fLoc.row, fLoc.col, false)); + waitForSwing(); + + Rectangle rect = listingPanel.getCursorBounds(); + + MouseEvent event = + new MouseEvent(fieldPanel, 0, System.currentTimeMillis(), 0, rect.x, rect.y, 0, false); + fieldPanel.getHoverHandler().mouseHovered(event); + waitForSwing(); + + captureProviderWithScreenShot(listingProvider); + } + + @Test + public void testCaptureVariableValueHoverPluginBrowser() throws Throwable { + CodeViewerProvider browserProvider = waitForComponentProvider(CodeViewerProvider.class); + prepareContext(); + + List

    stackRefs = new ArrayList<>(stackRefInstrs.keySet()); + Address refAddr = stackRefs.get(2); + Instruction ins = program.getListing().getInstructionAt(refAddr); + + ListingPanel listingPanel = browserProvider.getListingPanel(); + + Window window = moveProviderToItsOwnWindow(browserProvider, 1000, 600); + window.toFront(); + waitForSwing(); + + HoverLocation loc = + StackUnwinderTest.findOperandLocation(listingPanel, ins, new Scalar(32, 8)); + FieldLocation fLoc = loc.fLoc(); + BigInteger refIndex = listingPanel.getAddressIndexMap().getIndex(refAddr); + + FieldPanel fieldPanel = listingPanel.getFieldPanel(); + runSwing(() -> fieldPanel.goTo(refIndex, fLoc.fieldNum, fLoc.row, fLoc.col, false)); + waitForSwing(); + + Rectangle rect = listingPanel.getCursorBounds(); + + MouseEvent event = + new MouseEvent(fieldPanel, 0, System.currentTimeMillis(), 0, rect.x, rect.y, 0, false); + fieldPanel.getHoverHandler().mouseHovered(event); + waitForSwing(); + + captureProviderWithScreenShot(browserProvider); + } + + @Test + public void testCaptureVariableValueHoverPluginDecompiler() throws Throwable { + DecompilerProvider decompilerProvider = waitForComponentProvider(DecompilerProvider.class); + tool.showComponentProvider(decompilerProvider, true); + prepareContext(); + + Function function = program.getFunctionManager().getFunctionContaining(registerRefInstr); + + DecompilerPanel decompilerPanel = decompilerProvider.getDecompilerPanel(); + + Window window = moveProviderToItsOwnWindow(decompilerProvider, 600, 600); + window.toFront(); + waitForSwing(); + + HoverLocation loc = + StackUnwinderTest.findTokenLocation(decompilerPanel, function, "n", "if (1 < n) {"); + runSwing(() -> decompilerPanel.goToToken(loc.token())); + waitForSwing(); + + FieldPanel fieldPanel = decompilerPanel.getFieldPanel(); + Rectangle rect = fieldPanel.getCursorBounds(); + + MouseEvent event = + new MouseEvent(fieldPanel, 0, System.currentTimeMillis(), 0, rect.x, rect.y, 0, false); + fieldPanel.getHoverHandler().mouseHovered(event); + waitForSwing(); + + captureProviderWithScreenShot(decompilerProvider); + } +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/stack/StackUnwinderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/stack/StackUnwinderTest.java new file mode 100644 index 0000000000..c8ba1092d7 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/stack/StackUnwinderTest.java @@ -0,0 +1,1680 @@ +/* ### + * 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.stack; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.function.Predicate; + +import org.junit.Test; + +import docking.widgets.fieldpanel.Layout; +import docking.widgets.fieldpanel.field.Field; +import docking.widgets.fieldpanel.support.FieldLocation; +import generic.Unique; +import ghidra.app.decompiler.*; +import ghidra.app.decompiler.component.*; +import ghidra.app.plugin.assembler.*; +import ghidra.app.plugin.assembler.sleigh.sem.*; +import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand; +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; +import ghidra.app.plugin.core.debug.gui.stack.vars.*; +import ghidra.app.plugin.core.debug.gui.stack.vars.VariableValueRow.*; +import ghidra.app.plugin.core.debug.service.editing.DebuggerStateEditingServicePlugin; +import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePlugin; +import ghidra.app.plugin.core.debug.service.emulation.ProgramEmulationUtils; +import ghidra.app.plugin.core.debug.stack.StackUnwindWarning.*; +import ghidra.app.plugin.core.decompile.DecompilePlugin; +import ghidra.app.plugin.core.decompile.DecompilerProvider; +import ghidra.app.plugin.core.disassembler.DisassemblerPlugin; +import ghidra.app.services.*; +import ghidra.app.services.DebuggerEmulationService.EmulationResult; +import ghidra.app.services.DebuggerStateEditingService.StateEditingMode; +import ghidra.app.services.DebuggerStateEditingService.StateEditor; +import ghidra.app.util.viewer.field.FieldFactory; +import ghidra.app.util.viewer.field.ListingField; +import ghidra.app.util.viewer.listingpanel.ListingPanel; +import ghidra.lifecycle.Unfinished; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValue; +import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValuePcodeArithmetic; +import ghidra.pcode.exec.PcodeArithmetic.Purpose; +import ghidra.program.disassemble.Disassembler; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.data.*; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.listing.Function.FunctionUpdateType; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.program.model.scalar.Scalar; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.SourceType; +import ghidra.program.util.*; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.TraceLocation; +import ghidra.trace.model.breakpoint.TraceBreakpoint; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.listing.TraceData; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.schedule.Scheduler; +import ghidra.util.Msg; +import ghidra.util.NumericUtilities; +import ghidra.util.database.UndoableTransaction; + +public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerGUITest { + + public static final AssemblySelector NO_16BIT_CALLS = new AssemblySelector() { + @Override + public AssemblyResolvedPatterns select(AssemblyResolutionResults rr, + AssemblyPatternBlock ctx) throws AssemblySemanticException { + for (AssemblyResolvedPatterns res : filterCompatibleAndSort(rr, ctx)) { + byte[] ins = res.getInstruction().getVals(); + // HACK to avoid 16-bit CALL.... TODO: Why does this happen? + if (ins.length >= 2 && ins[0] == (byte) 0x66 && ins[1] == (byte) 0xe8) { + System.err.println( + "Filtered 16-bit call " + NumericUtilities.convertBytesToString(ins)); + continue; + } + return AssemblyResolution.resolved(res.getInstruction().fillMask(), + res.getContext(), "Selected", null, null, null); + } + throw new AssemblySemanticException(semanticErrors); + } + }; + + protected void createProgram(String languageID, String cSpecID) throws IOException { + Language language = getLanguageService().getLanguage(new LanguageID(languageID)); + CompilerSpec cSpec = cSpecID == null ? language.getDefaultCompilerSpec() + : language.getCompilerSpecByID(new CompilerSpecID(cSpecID)); + createProgram(language, cSpec); + } + + Address bodyInstr; + Address retInstr; + Address globalRefInstr; + Address stackRefInstr; + Address registerRefInstr; + Address funcInstr; + + CodeBrowserPlugin codeBrowserPlugin; + ListingPanel staticListing; + DebuggerListingPlugin listingPlugin; + ListingPanel dynamicListing; + DebuggerStateEditingService editingService; + DebuggerEmulationService emuService; + DecompilerProvider decompilerProvider; + DecompilerPanel decompilerPanel; + + protected Address stack(long offset) { + return program.getCompilerSpec().getStackSpace().getAddress(offset); + } + + protected Register register(String name) { + return program.getLanguage().getRegister(name); + } + + protected Function createSumSquaresProgramX86_32() throws Throwable { + createProgram("x86:LE:32:default", "gcc"); + intoProject(program); + try (UndoableTransaction tid = UndoableTransaction.start(program, "Assemble")) { + Address entry = addr(program, 0x00400000); + program.getMemory() + .createInitializedBlock(".text", entry, 0x1000, (byte) 0, monitor, false); + Assembler asm = Assemblers.getAssembler(program.getLanguage(), NO_16BIT_CALLS); + AssemblyBuffer buf = new AssemblyBuffer(asm, entry); + + buf.assemble("PUSH EBP"); + buf.assemble("MOV EBP, ESP"); + buf.assemble("SUB ESP, 0x10"); + + buf.assemble("XOR ECX,ECX"); + buf.assemble("MOV dword ptr [EBP+-4], ECX"); + buf.assemble("MOV dword ptr [EBP+-8], ECX"); + Address jumpCheck = buf.getNext(); + buf.assemble("JMP 0x" + buf.getNext()); + Address labelLoop = buf.getNext(); + buf.assemble("MOV EAX, dword ptr [EBP+-4]"); + bodyInstr = buf.getNext(); + buf.assemble("IMUL EAX, dword ptr [EBP+-4]"); + buf.assemble("ADD dword ptr [EBP+-8], EAX"); + + buf.assemble("INC dword ptr [EBP+-4]"); + Address labelCheck = buf.getNext(); + buf.assemble(jumpCheck, "JMP 0x" + labelCheck); + buf.assemble("MOV EAX, dword ptr [EBP+-4]"); + buf.assemble("CMP EAX, dword ptr [EBP+8]"); + buf.assemble("JLE 0x" + labelLoop); + + buf.assemble("MOV EAX, dword ptr [EBP+-8]"); + + buf.assemble("LEAVE"); + retInstr = buf.getNext(); + buf.assemble("RET"); + + byte[] bytes = buf.getBytes(); + program.getMemory().setBytes(entry, bytes); + + Disassembler dis = Disassembler.getDisassembler(program, monitor, null); + dis.disassemble(entry, null); + + Function function = program.getFunctionManager() + .createFunction("sumSquares", entry, + new AddressSet(entry, entry.add(bytes.length - 1)), + SourceType.USER_DEFINED); + + function.updateFunction("__cdecl", + new ReturnParameterImpl(IntegerDataType.dataType, program), + List.of( + new ParameterImpl("n", IntegerDataType.dataType, program)), + FunctionUpdateType.DYNAMIC_STORAGE_FORMAL_PARAMS, true, SourceType.ANALYSIS); + function.addLocalVariable( + new LocalVariableImpl("i", IntegerDataType.dataType, -8, program), + SourceType.USER_DEFINED); + function.addLocalVariable( + new LocalVariableImpl("sum", IntegerDataType.dataType, -12, program), + SourceType.USER_DEFINED); + return function; + } + } + + protected Function createFibonacciProgramX86_32() throws Throwable { + createProgram("x86:LE:32:default", "gcc"); + intoProject(program); + try (UndoableTransaction tid = UndoableTransaction.start(program, "Assemble")) { + Address entry = addr(program, 0x00400000); + program.getMemory() + .createInitializedBlock(".text", entry, 0x1000, (byte) 0, monitor, false); + Assembler asm = Assemblers.getAssembler(program.getLanguage(), NO_16BIT_CALLS); + AssemblyBuffer buf = new AssemblyBuffer(asm, entry); + + buf.assemble("PUSH EBP"); + buf.assemble("MOV EBP, ESP"); + + buf.assemble("CMP dword ptr [EBP+8], 1"); + Address jumpBase = buf.getNext(); + buf.assemble("JBE 0x" + buf.getNext()); + + // Recursive case. Let EDX be sum + // sum = fib(n - 1) + buf.assemble("MOV ECX, dword ptr [EBP+8]"); + buf.assemble("DEC ECX"); + buf.assemble("PUSH ECX"); // pass n - 1 + buf.assemble("CALL 0x" + entry); + buf.assemble("ADD ESP, 4"); // Clear parameters + registerRefInstr = buf.getNext(); + buf.assemble("MOV EDX, EAX"); + // sum += fib(n - 2) + buf.assemble("MOV ECX, dword ptr [EBP+8]"); + buf.assemble("SUB ECX, 2"); + buf.assemble("PUSH EDX"); // Caller Save EDX + buf.assemble("PUSH ECX"); // pass n - 2 + buf.assemble("CALL 0x" + entry); + buf.assemble("ADD ESP, 4"); // Clear parameters + buf.assemble("POP EDX"); // Restore EDX + buf.assemble("ADD EAX, EDX"); + + Address labelRet = buf.getNext(); + buf.assemble("LEAVE"); + retInstr = buf.getNext(); + buf.assemble("RET"); + + Address labelBase = buf.getNext(); + buf.assemble(jumpBase, "JBE 0x" + labelBase); + stackRefInstr = buf.getNext(); + buf.assemble("MOV EAX, dword ptr [EBP+8]"); + buf.assemble("JMP 0x" + labelRet); + + byte[] bytes = buf.getBytes(); + program.getMemory().setBytes(entry, bytes); + + Disassembler dis = Disassembler.getDisassembler(program, monitor, null); + dis.disassemble(entry, null); + + Function function = program.getFunctionManager() + .createFunction("fib", entry, + new AddressSet(entry, entry.add(bytes.length - 1)), + SourceType.USER_DEFINED); + + function.updateFunction("__cdecl", + new ReturnParameterImpl(UnsignedIntegerDataType.dataType, program), + List.of( + new ParameterImpl("n", UnsignedIntegerDataType.dataType, program)), + FunctionUpdateType.DYNAMIC_STORAGE_FORMAL_PARAMS, true, SourceType.ANALYSIS); + // NOTE: The decompiler doesn't actually use sum.... For some reason, it re-uses n + // Still, in the tests, I can use uVar1 (EAX) as a register variable + function.addLocalVariable( + new LocalVariableImpl("sum", 0, UnsignedIntegerDataType.dataType, register("EDX"), + program), + SourceType.USER_DEFINED); + + Instruction ins = program.getListing().getInstructionAt(stackRefInstr); + ins.addOperandReference(1, stack(4), RefType.READ, SourceType.ANALYSIS); + return function; + } + } + + protected Function createCallExternProgramX86_32() throws Throwable { + createProgram("x86:LE:32:default", "gcc"); + intoProject(program); + Address entry; + try (UndoableTransaction tid = UndoableTransaction.start(program, "Assemble")) { + entry = addr(program, 0x00400000); + Address externs = addr(program, 0x00700000); + program.getMemory() + .createInitializedBlock(".text", entry, 0x1000, (byte) 0, monitor, false); + program.getMemory() + .createUninitializedBlock(MemoryBlock.EXTERNAL_BLOCK_NAME, externs, 0x10, + false); + + program.getFunctionManager() + .createFunction("myExtern", externs, new AddressSet(externs), + SourceType.USER_DEFINED); + + Assembler asm = Assemblers.getAssembler(program.getLanguage(), NO_16BIT_CALLS); + AssemblyBuffer buf = new AssemblyBuffer(asm, entry); + + buf.assemble("PUSH EBP"); + buf.assemble("MOV EBP, ESP"); + buf.assemble("SUB ESP, 0x10"); + + buf.assemble("XOR ECX,ECX"); + buf.assemble("MOV dword ptr [EBP+-4], ECX"); + buf.assemble("MOV dword ptr [EBP+-8], ECX"); + Address jumpCheck = buf.getNext(); + buf.assemble("JMP 0x" + buf.getNext()); + Address labelLoop = buf.getNext(); + buf.assemble("MOV EAX, dword ptr [EBP+-4]"); + bodyInstr = buf.getNext(); + buf.assemble("PUSH EAX"); + buf.assemble("CALL 0x" + externs); + buf.assemble("ADD ESP, 0x4"); + buf.assemble("ADD dword ptr [EBP+-8], EAX"); + + buf.assemble("INC dword ptr [EBP+-4]"); + Address labelCheck = buf.getNext(); + buf.assemble(jumpCheck, "JMP 0x" + labelCheck); + buf.assemble("MOV EAX, dword ptr [EBP+-4]"); + buf.assemble("CMP EAX, dword ptr [EBP+8]"); + buf.assemble("JL 0x" + labelLoop); + + buf.assemble("MOV EAX, dword ptr [EBP+-8]"); + + buf.assemble("LEAVE"); + buf.assemble("RET"); + + byte[] bytes = buf.getBytes(); + program.getMemory().setBytes(entry, bytes); + + Disassembler dis = Disassembler.getDisassembler(program, monitor, null); + dis.disassemble(entry, null); + + return program.getFunctionManager() + .createFunction("sumReturns", entry, + new AddressSet(entry, entry.add(bytes.length - 1)), + SourceType.USER_DEFINED); + } + } + + protected Function createCallPointerProgramX86_32() throws Throwable { + createProgram("x86:LE:32:default", "gcc"); + intoProject(program); + Address entry; + try (UndoableTransaction tid = UndoableTransaction.start(program, "Assemble")) { + entry = addr(program, 0x00400000); + program.getMemory() + .createInitializedBlock(".text", entry, 0x1000, (byte) 0, monitor, false); + + Assembler asm = Assemblers.getAssembler(program.getLanguage(), NO_16BIT_CALLS); + AssemblyBuffer buf = new AssemblyBuffer(asm, entry); + + buf.assemble("PUSH EBP"); + buf.assemble("MOV EBP, ESP"); + buf.assemble("SUB ESP, 0x10"); + + buf.assemble("XOR ECX,ECX"); + buf.assemble("MOV dword ptr [EBP+-4], ECX"); + buf.assemble("MOV dword ptr [EBP+-8], ECX"); + Address jumpCheck = buf.getNext(); + buf.assemble("JMP 0x" + buf.getNext()); + Address labelLoop = buf.getNext(); + buf.assemble("MOV EDX, dword ptr [EBP+-4]"); + buf.assemble("MOV EAX, dword ptr [EBP+12]"); + bodyInstr = buf.getNext(); + buf.assemble("PUSH EDX"); + buf.assemble("CALL EAX"); + buf.assemble("ADD ESP, 0x4"); + buf.assemble("ADD dword ptr [EBP+-8], EAX"); + + buf.assemble("INC dword ptr [EBP+-4]"); + Address labelCheck = buf.getNext(); + buf.assemble(jumpCheck, "JMP 0x" + labelCheck); + buf.assemble("MOV EAX, dword ptr [EBP+-4]"); + buf.assemble("CMP EAX, dword ptr [EBP+8]"); + buf.assemble("JL 0x" + labelLoop); + + buf.assemble("MOV EAX, dword ptr [EBP+-8]"); + + buf.assemble("LEAVE"); + buf.assemble("RET"); + + byte[] bytes = buf.getBytes(); + program.getMemory().setBytes(entry, bytes); + + Disassembler dis = Disassembler.getDisassembler(program, monitor, null); + dis.disassemble(entry, null); + + return program.getFunctionManager() + .createFunction("sumReturns", entry, + new AddressSet(entry, entry.add(bytes.length - 1)), + SourceType.USER_DEFINED); + } + } + + protected Function createSetGlobalProgramX86_32() throws Throwable { + createProgram("x86:LE:32:default", "gcc"); + intoProject(program); + + try (UndoableTransaction tid = UndoableTransaction.start(program, "Assemble")) { + Address entry = addr(program, 0x00400000); + program.getMemory() + .createInitializedBlock(".text", entry, 0x1000, (byte) 0, monitor, false); + Address global = addr(program, 0x00600000); + program.getMemory() + .createInitializedBlock(".data", global, 0x1000, (byte) 0, monitor, false); + + Assembler asm = Assemblers.getAssembler(program.getLanguage(), NO_16BIT_CALLS); + AssemblyBuffer buf = new AssemblyBuffer(asm, entry); + + buf.assemble("MOV EAX, 0xdeadbeef"); + globalRefInstr = buf.getNext(); + buf.assemble("MOV dword ptr [0x00600000], EAX"); + retInstr = buf.getNext(); + buf.assemble("RET"); + + byte[] bytes = buf.getBytes(); + program.getMemory().setBytes(entry, bytes); + + Disassembler dis = Disassembler.getDisassembler(program, monitor, null); + dis.disassemble(entry, null); + + program.getListing().createData(global, IntegerDataType.dataType, 4); + program.getSymbolTable().createLabel(global, "myGlobal", SourceType.USER_DEFINED); + + return program.getFunctionManager() + .createFunction("setGlobal", entry, + new AddressSet(entry, entry.add(bytes.length - 1)), + SourceType.USER_DEFINED); + } + } + + protected Function createFillStructProgramX86_32() throws Throwable { + createProgram("x86:LE:32:default", "gcc"); + intoProject(program); + + try (UndoableTransaction tid = UndoableTransaction.start(program, "Assemble")) { + ProgramBasedDataTypeManager dtm = program.getDataTypeManager(); + Structure structure = new StructureDataType("MyStruct", 0, dtm); + structure.add(WordDataType.dataType, "y", ""); + structure.add(ByteDataType.dataType, "m", ""); + structure.add(ByteDataType.dataType, "d", ""); + structure = + (Structure) dtm.addDataType(structure, DataTypeConflictHandler.DEFAULT_HANDLER); + + Address entry = addr(program, 0x00400000); + program.getMemory() + .createInitializedBlock(".text", entry, 0x1000, (byte) 0, monitor, false); + Address global = addr(program, 0x00600000); + MemoryBlock dataBlock = program.getMemory() + .createInitializedBlock(".data", global, 0x1000, (byte) 0, monitor, false); + dataBlock.setWrite(true); + + Assembler asm = Assemblers.getAssembler(program.getLanguage(), NO_16BIT_CALLS); + AssemblyBuffer buf = new AssemblyBuffer(asm, entry); + + buf.assemble("PUSH EBP"); + buf.assemble("MOV EBP, ESP"); + buf.assemble("SUB ESP, 4"); + buf.assemble("LEA EAX, [0x00600000]"); + buf.assemble("PUSH EAX"); + Address call1 = buf.getNext(); + buf.assemble("CALL 0x" + buf.getNext()); + buf.assemble("ADD ESP, 4"); + buf.assemble("LEA EAX, [ESP]"); + buf.assemble("PUSH EAX"); + Address call2 = buf.getNext(); + buf.assemble("CALL 0x" + buf.getNext()); + buf.assemble("ADD ESP, 4"); + buf.assemble("MOVZX EAX, word ptr [ESP]"); + buf.assemble("MOVZX EDX, byte ptr [0x00600002]"); + buf.assemble("ADD EAX, EDX"); + buf.assemble("LEAVE"); + buf.assemble("RET"); + + funcInstr = buf.getNext(); + buf.assemble(call1, "CALL 0x" + funcInstr); + buf.assemble(call2, "CALL 0x" + funcInstr); + + buf.assemble("MOV EAX, dword ptr [ESP+4]"); + buf.assemble("MOV word ptr [EAX], 2022"); + buf.assemble("MOV byte ptr [EAX+2], 12"); + buf.assemble("MOV byte ptr [EAX+3], 9"); + retInstr = buf.getNext(); + buf.assemble("RET"); + Address end = buf.getNext(); + + byte[] bytes = buf.getBytes(); + program.getMemory().setBytes(entry, bytes); + + Disassembler dis = Disassembler.getDisassembler(program, monitor, null); + dis.disassemble(entry, null); + + program.getListing().createData(global, structure, 4); + program.getSymbolTable().createLabel(global, "myGlobal", SourceType.USER_DEFINED); + + Function funFillStruct = program.getFunctionManager() + .createFunction("fillStruct", funcInstr, + new AddressSet(funcInstr, end.previous()), SourceType.USER_DEFINED); + funFillStruct.updateFunction("__cdecl", null, + List.of( + new ParameterImpl("s", new PointerDataType(structure), program)), + FunctionUpdateType.DYNAMIC_STORAGE_FORMAL_PARAMS, true, SourceType.ANALYSIS); + + Function main = program.getFunctionManager() + .createFunction("main", entry, new AddressSet(entry, funcInstr.previous()), + SourceType.USER_DEFINED); + main.updateFunction("__cdecl", + new ReturnParameterImpl(DWordDataType.dataType, program), + FunctionUpdateType.DYNAMIC_STORAGE_FORMAL_PARAMS, true, SourceType.ANALYSIS); + main.addLocalVariable( + new LocalVariableImpl("myStack", structure, -8, program), + SourceType.ANALYSIS); + return main; + } + } + + protected Function createFillStructArrayProgramX86_32() throws Throwable { + createProgram("x86:LE:32:default", "gcc"); + intoProject(program); + + try (UndoableTransaction tid = UndoableTransaction.start(program, "Assemble")) { + ProgramBasedDataTypeManager dtm = program.getDataTypeManager(); + Structure structure = new StructureDataType("MyStruct", 0, dtm); + structure.add(WordDataType.dataType, "y", ""); + structure.add(ByteDataType.dataType, "m", ""); + structure.add(ByteDataType.dataType, "d", ""); + structure = + (Structure) dtm.addDataType(structure, DataTypeConflictHandler.DEFAULT_HANDLER); + + Address entry = addr(program, 0x00400000); + program.getMemory() + .createInitializedBlock(".text", entry, 0x1000, (byte) 0, monitor, false); + Address global = addr(program, 0x00600000); + MemoryBlock dataBlock = program.getMemory() + .createInitializedBlock(".data", global, 0x1000, (byte) 0, monitor, false); + dataBlock.setWrite(true); + + Assembler asm = Assemblers.getAssembler(program.getLanguage(), NO_16BIT_CALLS); + AssemblyBuffer buf = new AssemblyBuffer(asm, entry); + + buf.assemble("PUSH EBP"); + buf.assemble("MOV EBP, ESP"); + buf.assemble("PUSH 2"); + buf.assemble("LEA EAX, [0x00600000]"); + buf.assemble("PUSH EAX"); + Address call = buf.getNext(); + buf.assemble("CALL 0x" + buf.getNext()); + buf.assemble("ADD ESP, 8"); + buf.assemble("LEAVE"); + buf.assemble("RET"); + + funcInstr = buf.getNext(); + buf.assemble(call, "CALL 0x" + funcInstr); + + buf.assemble("MOV EAX, dword ptr [ESP+4]"); + buf.assemble("MOV ECX, dword ptr [ESP+8]"); + //buf.assemble("LEA EAX, [EAX+ECX*4]"); + buf.assemble("MOV word ptr [EAX+ECX*4], 2022"); + buf.assemble("MOV byte ptr [EAX+ECX*4+2], 12"); + buf.assemble("MOV byte ptr [EAX+ECX*4+3], 9"); + retInstr = buf.getNext(); + buf.assemble("RET"); + Address end = buf.getNext(); + + byte[] bytes = buf.getBytes(); + program.getMemory().setBytes(entry, bytes); + + Disassembler dis = Disassembler.getDisassembler(program, monitor, null); + dis.disassemble(entry, null); + + program.getListing() + .createData(global, + new ArrayDataType(structure, 10, 4, program.getDataTypeManager())); + program.getSymbolTable().createLabel(global, "myGlobal", SourceType.USER_DEFINED); + + Function funFillStruct = program.getFunctionManager() + .createFunction("fillStruct", funcInstr, + new AddressSet(funcInstr, end.previous()), SourceType.USER_DEFINED); + funFillStruct.updateFunction("__cdecl", null, + List.of( + new ParameterImpl("s", new PointerDataType(structure), program), + new ParameterImpl("i", IntegerDataType.dataType, program)), + FunctionUpdateType.DYNAMIC_STORAGE_FORMAL_PARAMS, true, SourceType.ANALYSIS); + + Function main = program.getFunctionManager() + .createFunction("main", entry, new AddressSet(entry, funcInstr.previous()), + SourceType.USER_DEFINED); + main.updateFunction("__cdecl", + new ReturnParameterImpl(DWordDataType.dataType, program), + FunctionUpdateType.DYNAMIC_STORAGE_FORMAL_PARAMS, true, SourceType.ANALYSIS); + return main; + } + } + + @Test + public void testComputeUnwindInfoX86_32() throws Throwable { + addPlugin(tool, CodeBrowserPlugin.class); + + Function function = createSumSquaresProgramX86_32(); + Address entry = function.getEntryPoint(); + + programManager.openProgram(program); + + UnwindAnalysis ua = new UnwindAnalysis(program); + + UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor); + assertEquals( + new UnwindInfo(function, 0, 4, stack(0), Map.of(), new StackUnwindWarningSet()), + infoAtEntry); + + UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor); + assertEquals(new UnwindInfo(function, -20, 4, stack(0), + Map.of( + register("EBP"), stack(-4)), + new StackUnwindWarningSet()), + infoAtBody); + } + + @Test + public void testComputeUnwindInfoWithExternCallsX86_32() throws Throwable { + addPlugin(tool, CodeBrowserPlugin.class); + + Function function = createCallExternProgramX86_32(); + Address entry = function.getEntryPoint(); + Function myExtern = + (Function) Unique.assertOne(program.getSymbolTable().getSymbols("myExtern")) + .getObject(); + + programManager.openProgram(program); + + UnwindAnalysis ua = new UnwindAnalysis(program); + + UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor); + assertEquals( + new UnwindInfo(function, 0, 4, stack(0), Map.of(), new StackUnwindWarningSet()), + infoAtEntry); + + UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor); + assertEquals(new UnwindInfo(function, -20, 4, stack(0), + Map.of( + register("EBP"), stack(-4)), + new StackUnwindWarningSet( + new UnspecifiedConventionStackUnwindWarning(myExtern), + new UnknownPurgeStackUnwindWarning(myExtern))), + infoAtBody); + } + + @Test + public void testComputeUnwindInfoWithIndirectCallsX86_32() throws Throwable { + addPlugin(tool, CodeBrowserPlugin.class); + addPlugin(tool, DecompilePlugin.class); + + Function function = createCallPointerProgramX86_32(); + Address entry = function.getEntryPoint(); + + programManager.openProgram(program); + + UnwindAnalysis ua = new UnwindAnalysis(program); + + UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor); + assertEquals( + new UnwindInfo(function, 0, 4, stack(0), Map.of(), new StackUnwindWarningSet()), + infoAtEntry); + + UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor); + DataType ptr2Undef = new PointerDataType(DataType.DEFAULT, program.getDataTypeManager()); + assertEquals(new UnwindInfo(function, -20, 4, stack(0), + Map.of( + register("EBP"), stack(-4)), + new StackUnwindWarningSet( + new UnexpectedTargetTypeStackUnwindWarning(ptr2Undef))), + infoAtBody); + } + + @Test + public void testUnwindTopFrameX86_32() throws Throwable { + addPlugin(tool, CodeBrowserPlugin.class); + addPlugin(tool, DebuggerListingPlugin.class); + addPlugin(tool, DisassemblerPlugin.class); + addPlugin(tool, DecompilePlugin.class); + DebuggerStateEditingService editingService = + addPlugin(tool, DebuggerStateEditingServicePlugin.class); + DebuggerEmulationService emuService = addPlugin(tool, DebuggerEmulationServicePlugin.class); + + Function function = createSumSquaresProgramX86_32(); + Address entry = function.getEntryPoint(); + + programManager.openProgram(program); + + useTrace(ProgramEmulationUtils.launchEmulationTrace(program, entry, this)); + tb.trace.release(this); + TraceThread thread = Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + traceManager.openTrace(tb.trace); + traceManager.activateThread(thread); + waitForSwing(); + + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + StateEditor editor = editingService.createStateEditor(tb.trace); + + DebuggerCoordinates atSetup = traceManager.getCurrent(); + StackUnwinder unwinder = new StackUnwinder(tool, atSetup.getPlatform()); + AnalysisUnwoundFrame frameAtSetup = unwinder.start(atSetup, monitor); + + Parameter param1 = function.getParameter(0); + waitOn(frameAtSetup.setValue(editor, param1, BigInteger.valueOf(4))); + waitForTasks(); + + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getBreakpointManager() + .addBreakpoint("Breakpoints[0]", Lifespan.nowOn(0), retInstr, Set.of(), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "capture return value"); + } + + EmulationResult result = emuService.run(atSetup.getPlatform(), atSetup.getTime(), monitor, + Scheduler.oneThread(thread)); + Msg.debug(this, "Broke after " + result.schedule()); + + traceManager.activateTime(result.schedule()); + waitForTasks(); + DebuggerCoordinates after = traceManager.getCurrent(); + AnalysisUnwoundFrame frameAfter = unwinder.start(after, monitor); + + WatchValuePcodeArithmetic wa = + WatchValuePcodeArithmetic.forLanguage(after.getPlatform().getLanguage()); + for (Variable variable : function.getVariables(null)) { + BigInteger value = wa.toBigInteger(frameAfter.getValue(variable), Purpose.INSPECT); + Msg.debug(this, variable + " = " + value); + } + Variable retVar = function.getReturn(); + BigInteger retVal = wa.toBigInteger(frameAfter.getValue(retVar), Purpose.INSPECT); + Msg.debug(this, "Return " + retVal); + assertEquals(BigInteger.valueOf(30), retVal); + } + + @Test + public void testUnwindRecursiveX86_32() throws Throwable { + addPlugin(tool, CodeBrowserPlugin.class); + addPlugin(tool, DebuggerListingPlugin.class); + addPlugin(tool, DisassemblerPlugin.class); + addPlugin(tool, DecompilePlugin.class); + DebuggerStateEditingService editingService = + addPlugin(tool, DebuggerStateEditingServicePlugin.class); + DebuggerEmulationService emuService = addPlugin(tool, DebuggerEmulationServicePlugin.class); + + Function function = createFibonacciProgramX86_32(); + Address entry = function.getEntryPoint(); + + programManager.openProgram(program); + + useTrace(ProgramEmulationUtils.launchEmulationTrace(program, entry, this)); + tb.trace.release(this); + TraceThread thread = Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + traceManager.openTrace(tb.trace); + traceManager.activateThread(thread); + waitForSwing(); + + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + StateEditor editor = editingService.createStateEditor(tb.trace); + + DebuggerCoordinates atSetup = traceManager.getCurrent(); + StackUnwinder unwinder = new StackUnwinder(tool, atSetup.getPlatform()); + AnalysisUnwoundFrame frameAtSetup = unwinder.start(atSetup, monitor); + + Parameter param1 = function.getParameter(0); + waitOn(frameAtSetup.setValue(editor, param1, BigInteger.valueOf(9))); + waitOn(frameAtSetup.setReturnAddress(editor, tb.addr(0xdeadbeef))); + waitForTasks(); + + TraceBreakpoint bptUnwind; + try (UndoableTransaction tid = tb.startTransaction()) { + bptUnwind = tb.trace.getBreakpointManager() + .addBreakpoint("Breakpoints[0]", Lifespan.nowOn(0), retInstr, + Set.of(), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "unwind stack"); + tb.trace.getBreakpointManager() + .addBreakpoint("Breakpoints[1]", Lifespan.nowOn(0), tb.addr(0xdeadbeef), + Set.of(), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "capture return value"); + } + + EmulationResult result = emuService.run(atSetup.getPlatform(), atSetup.getTime(), monitor, + Scheduler.oneThread(thread)); + Msg.debug(this, "Broke after " + result.schedule()); + + traceManager.activateTime(result.schedule()); + waitForTasks(); + DebuggerCoordinates tallest = traceManager.getCurrent(); + AnalysisUnwoundFrame frameTallest = unwinder.start(tallest, monitor); + while (true) { + Msg.debug(this, "Frame " + frameTallest.getLevel()); + for (Variable variable : function.getVariables(null)) { + try { + WatchValue value = frameTallest.getValue(variable); + Msg.debug(this, " " + variable + " = " + value); + } + catch (UnwindException e) { + Msg.debug(this, " Cannot get " + variable + ": " + e); + } + } + try { + frameTallest = frameTallest.unwindNext(monitor); + } + catch (NoSuchElementException e) { + break; + } + } + + try (UndoableTransaction tid = tb.startTransaction()) { + bptUnwind.delete(); + } + + result = emuService.run(tallest.getPlatform(), tallest.getTime(), monitor, + Scheduler.oneThread(thread)); + + // Step back, so PC is in the function + traceManager.activateTime(result.schedule().steppedBackward(tb.trace, 1)); + waitForTasks(); + DebuggerCoordinates after = traceManager.getCurrent(); + AnalysisUnwoundFrame frameAfter = unwinder.start(after, monitor); + + for (Variable variable : function.getVariables(null)) { + WatchValue value = frameAfter.getValue(variable); + Msg.debug(this, variable + " = " + value); + } + Variable retVar = function.getReturn(); + WatchValue retVal = frameAfter.getValue(retVar); + Msg.debug(this, "Return " + retVal); + assertEquals(BigInteger.valueOf(34), retVal.toBigInteger(false)); + } + + @Test + public void testCreateFramesAtEntryX86_32() throws Throwable { + addPlugin(tool, CodeBrowserPlugin.class); + addPlugin(tool, DebuggerListingPlugin.class); + addPlugin(tool, DisassemblerPlugin.class); + addPlugin(tool, DecompilePlugin.class); + + Function function = createFibonacciProgramX86_32(); + Address entry = function.getEntryPoint(); + + programManager.openProgram(program); + + useTrace(ProgramEmulationUtils.launchEmulationTrace(program, entry, this)); + tb.trace.release(this); + TraceThread thread = Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + traceManager.openTrace(tb.trace); + traceManager.activateThread(thread); + waitForSwing(); + + DebuggerCoordinates atSetup = traceManager.getCurrent(); + try (UndoableTransaction tid = tb.startTransaction()) { + new UnwindStackCommand(tool, atSetup).applyTo(tb.trace, monitor); + } + waitForDomainObject(tb.trace); + + ListingUnwoundFrame frame = VariableValueUtils.locateFrame(tool, atSetup, function); + TraceData data = frame.getData(); + assertEquals(8, data.getLength()); + assertEquals(2, data.getNumComponents()); + assertEquals("return_address", data.getComponent(0).getFieldName()); + assertEquals("param_n", data.getComponent(1).getFieldName()); + + assertNull(VariableValueUtils.locateFrame(tool, atSetup.frame(1), function)); + } + + protected static void assertField(Data data, String name, Object value) { + assertEquals(name, data.getFieldName()); + assertEquals(value, data.getValue()); + } + + protected void addPlugins() throws Throwable { + codeBrowserPlugin = addPlugin(tool, CodeBrowserPlugin.class); + staticListing = codeBrowserPlugin.getProvider().getListingPanel(); + listingPlugin = addPlugin(tool, DebuggerListingPlugin.class); + dynamicListing = listingPlugin.getProvider().getListingPanel(); + addPlugin(tool, DisassemblerPlugin.class); + addPlugin(tool, DecompilePlugin.class); + editingService = addPlugin(tool, DebuggerStateEditingServicePlugin.class); + emuService = addPlugin(tool, DebuggerEmulationServicePlugin.class); + + decompilerProvider = waitForComponentProvider(DecompilerProvider.class); + decompilerPanel = decompilerProvider.getDecompilerPanel(); + } + + protected Function runToTallestRecursionAndCreateFrames(int n) throws Throwable { + addPlugins(); + + Function function = createFibonacciProgramX86_32(); + Address entry = function.getEntryPoint(); + + programManager.openProgram(program); + + useTrace(ProgramEmulationUtils.launchEmulationTrace(program, entry, this)); + tb.trace.release(this); + TraceThread thread = Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + traceManager.openTrace(tb.trace); + traceManager.activateThread(thread); + waitForSwing(); + + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + StateEditor editor = editingService.createStateEditor(tb.trace); + + DebuggerCoordinates atSetup = traceManager.getCurrent(); + StackUnwinder unwinder = new StackUnwinder(tool, atSetup.getPlatform()); + AnalysisUnwoundFrame frameAtSetup = unwinder.start(atSetup, monitor); + + Parameter paramN = function.getParameter(0); + assertEquals("n", paramN.getName()); // Sanity + waitOn(frameAtSetup.setValue(editor, paramN, BigInteger.valueOf(n))); + waitOn(frameAtSetup.setReturnAddress(editor, tb.addr(0xdeadbeef))); + waitForTasks(); + + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getBreakpointManager() + .addBreakpoint("Breakpoints[0]", Lifespan.nowOn(0), retInstr, + Set.of(), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "unwind stack"); + } + + EmulationResult result = emuService.run(atSetup.getPlatform(), atSetup.getTime(), monitor, + Scheduler.oneThread(thread)); + Msg.debug(this, "Broke after " + result.schedule()); + + traceManager.activateTime(result.schedule()); + waitForTasks(); + DebuggerCoordinates tallest = traceManager.getCurrent(); + try (UndoableTransaction tid = tb.startTransaction()) { + new UnwindStackCommand(tool, tallest).applyTo(tb.trace, monitor); + } + waitForDomainObject(tb.trace); + + return function; + } + + protected Function runToRetSetGlobalAndCreateFrames() throws Throwable { + addPlugins(); + + Function function = createSetGlobalProgramX86_32(); + Address entry = function.getEntryPoint(); + + programManager.openProgram(program); + + useTrace(ProgramEmulationUtils.launchEmulationTrace(program, entry, this)); + tb.trace.release(this); + TraceThread thread = Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + traceManager.openTrace(tb.trace); + traceManager.activateThread(thread); + waitForSwing(); + + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + StateEditor editor = editingService.createStateEditor(tb.trace); + // Move stack where it shows in UI. Not required, but nice for debugging. + Register sp = program.getCompilerSpec().getStackPointer(); + waitOn(editor.setRegister(new RegisterValue(sp, BigInteger.valueOf(0x4ff0)))); + + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getBreakpointManager() + .addBreakpoint("Breakpoints[0]", Lifespan.nowOn(0), retInstr, + Set.of(), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "unwind stack"); + } + + DebuggerCoordinates atSetup = traceManager.getCurrent(); + EmulationResult result = emuService.run(atSetup.getPlatform(), atSetup.getTime(), monitor, + Scheduler.oneThread(thread)); + Msg.debug(this, "Broke after " + result.schedule()); + + traceManager.activateTime(result.schedule()); + waitForTasks(); + DebuggerCoordinates atRet = traceManager.getCurrent(); + try (UndoableTransaction tid = tb.startTransaction()) { + new UnwindStackCommand(tool, atRet).applyTo(tb.trace, monitor); + } + waitForDomainObject(tb.trace); + + return function; + } + + protected Function runToRetFillStructAndCreateFrames() throws Throwable { + addPlugins(); + + Function function = createFillStructProgramX86_32(); + Address entry = function.getEntryPoint(); + + programManager.openProgram(program); + + useTrace(ProgramEmulationUtils.launchEmulationTrace(program, entry, this)); + tb.trace.release(this); + TraceThread thread = Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + traceManager.openTrace(tb.trace); + traceManager.activateThread(thread); + waitForSwing(); + + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + StateEditor editor = editingService.createStateEditor(tb.trace); + // Move stack where it shows in UI. Not required, but nice for debugging. + Register sp = program.getCompilerSpec().getStackPointer(); + waitOn(editor.setRegister(new RegisterValue(sp, BigInteger.valueOf(0x4ff0)))); + + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getBreakpointManager() + .addBreakpoint("Breakpoints[0]", Lifespan.nowOn(0), retInstr, + Set.of(), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "unwind stack"); + } + + DebuggerCoordinates atSetup = traceManager.getCurrent(); + EmulationResult result = emuService.run(atSetup.getPlatform(), atSetup.getTime(), monitor, + Scheduler.oneThread(thread)); + Msg.debug(this, "Broke after " + result.schedule()); + + traceManager.activateTime(result.schedule()); + waitForTasks(); + DebuggerCoordinates atRet = traceManager.getCurrent(); + try (UndoableTransaction tid = tb.startTransaction()) { + new UnwindStackCommand(tool, atRet).applyTo(tb.trace, monitor); + } + waitForDomainObject(tb.trace); + + return function; + } + + protected Function runToRetFillStructArrayAndCreateFrames() throws Throwable { + addPlugins(); + + Function function = createFillStructArrayProgramX86_32(); + Address entry = function.getEntryPoint(); + + programManager.openProgram(program); + + useTrace(ProgramEmulationUtils.launchEmulationTrace(program, entry, this)); + tb.trace.release(this); + TraceThread thread = Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + traceManager.openTrace(tb.trace); + traceManager.activateThread(thread); + waitForSwing(); + + editingService.setCurrentMode(tb.trace, StateEditingMode.WRITE_TRACE); + StateEditor editor = editingService.createStateEditor(tb.trace); + // Move stack where it shows in UI. Not required, but nice for debugging. + Register sp = program.getCompilerSpec().getStackPointer(); + waitOn(editor.setRegister(new RegisterValue(sp, BigInteger.valueOf(0x4ff0)))); + + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getBreakpointManager() + .addBreakpoint("Breakpoints[0]", Lifespan.nowOn(0), retInstr, + Set.of(), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "unwind stack"); + } + + DebuggerCoordinates atSetup = traceManager.getCurrent(); + EmulationResult result = emuService.run(atSetup.getPlatform(), atSetup.getTime(), monitor, + Scheduler.oneThread(thread)); + Msg.debug(this, "Broke after " + result.schedule()); + + traceManager.activateTime(result.schedule()); + waitForTasks(); + DebuggerCoordinates atRet = traceManager.getCurrent(); + try (UndoableTransaction tid = tb.startTransaction()) { + new UnwindStackCommand(tool, atRet).applyTo(tb.trace, monitor); + } + waitForDomainObject(tb.trace); + + return function; + } + + @Test + public void testCreateFramesTallestX86_32() throws Throwable { + Function function = runToTallestRecursionAndCreateFrames(9); + DebuggerCoordinates tallest = traceManager.getCurrent(); + + ListingUnwoundFrame frame; + TraceData data; + + frame = VariableValueUtils.locateFrame(tool, tallest, function); + data = frame.getData(); + assertEquals(8, data.getLength()); + assertEquals(2, data.getNumComponents()); + assertEquals(tb.addr(0x40002c), frame.getProgramCounter()); + assertEquals(tb.addr(0x4fa0), frame.getBasePointer()); + // Saved EBP has already been popped by LEAVE + assertField(data.getComponent(0), "return_address", tb.addr(0x400013)); + assertField(data.getComponent(1), "param_n", new Scalar(32, 1)); + + frame = VariableValueUtils.locateFrame(tool, tallest.frame(1), function); + data = frame.getData(); + assertEquals(12, data.getLength()); + assertEquals(3, data.getNumComponents()); + assertEquals(tb.addr(0x400013), frame.getProgramCounter()); + assertEquals(tb.addr(0x4fac), frame.getBasePointer()); + assertField(data.getComponent(0), "saved_EBP", new Scalar(32, 0x4fb4)); + assertField(data.getComponent(1), "return_address", tb.addr(0x400013)); + assertField(data.getComponent(2), "param_n", new Scalar(32, 2)); + + assertNotNull(VariableValueUtils.locateFrame(tool, tallest.frame(8), function)); + assertNull(VariableValueUtils.locateFrame(tool, tallest.frame(9), function)); + } + + public record HoverLocation(ProgramLocation pLoc, FieldLocation fLoc, Field field, + ClangToken token) { + } + + public static HoverLocation findLocation(ListingPanel panel, + Address address, Class locType, Predicate predicate) { + Layout layout = panel.getLayout(address); + int numFields = layout.getNumFields(); + for (int i = 0; i < numFields; i++) { + Field field = layout.getField(i); + if (!(field instanceof ListingField listingField)) { + continue; + } + FieldFactory factory = listingField.getFieldFactory(); + int numRows = field.getNumRows(); + for (int r = 0; r < numRows; r++) { + int numCols = field.getNumCols(r); + for (int c = 0; c < numCols; c++) { + ProgramLocation loc = factory.getProgramLocation(r, c, listingField); + if (!locType.isInstance(loc)) { + continue; + } + if (!predicate.test(locType.cast(loc))) { + continue; + } + return new HoverLocation(loc, new FieldLocation(0, i, r, c), field, null); + } + } + } + return null; + } + + public static HoverLocation findVariableLocation(ListingPanel panel, Function function, + String name) { + return findLocation(panel, function.getEntryPoint(), VariableLocation.class, + varLoc -> name.equals(varLoc.getVariable().getName())); + } + + public static HoverLocation findOperandLocation(ListingPanel panel, Instruction ins, + Object operand) { + return findLocation(panel, ins.getAddress(), OperandFieldLocation.class, opLoc -> { + int subIdx = opLoc.getSubOperandIndex(); + if (subIdx == -1) { + return false; + } + return operand.equals( + ins.getDefaultOperandRepresentationList(opLoc.getOperandIndex()).get(subIdx)); + }); + } + + protected static void assertTable(Map texts, VariableValueTable table) { + ErrorRow error = (ErrorRow) table.get(RowKey.ERROR); + if (error != null && !texts.containsKey(RowKey.ERROR)) { + throw new AssertionError("ErrorRow present", error.error()); + } + for (Map.Entry ent : texts.entrySet()) { + RowKey key = ent.getKey(); + VariableValueRow row = table.get(key); + assertNotNull("Missing " + key, row); + if (key != RowKey.WARNINGS) { + assertEquals(ent.getValue(), row.toSimpleString()); + } + } + assertEquals(texts.size(), table.getNumRows()); + } + + protected VariableValueTable getVariableValueTable(VariableValueHoverService valuesService, + ProgramLocation programLocation, DebuggerCoordinates current, + FieldLocation fieldLocation, Field field) throws Throwable { + VariableValueTable table = new VariableValueTable(); + List warnings = new ArrayList<>(); + waitOn(valuesService.fillVariableValueTable(table, programLocation, current, + fieldLocation, field, warnings)); + table.add(new WarningsRow(warnings)); + return table; + } + + @Test + public void testStackVariableHover() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + Function function = runToTallestRecursionAndCreateFrames(2); + HoverLocation loc = findVariableLocation(staticListing, function, "n"); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: n", + RowKey.FRAME, "Frame: 0 fib pc=0040002c sp=00004ff4 base=00004ff4", + RowKey.STORAGE, "Storage: Stack[0x4]:4", + RowKey.TYPE, "Type: uint", + RowKey.LOCATION, "Location: 00004ff8:4", + RowKey.BYTES, "Bytes: (KNOWN) 01 00 00 00", + RowKey.INTEGER, "Integer: (KNOWN) 1", + RowKey.VALUE, "Value: (KNOWN) 1h", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testRegisterVariableHover() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + Function function = runToTallestRecursionAndCreateFrames(2); + HoverLocation loc = findVariableLocation(staticListing, function, "sum"); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: sum", + RowKey.FRAME, "Frame: 0 fib pc=0040002c sp=00004ff4 base=00004ff4", + RowKey.STORAGE, "Storage: EDX:4", + RowKey.TYPE, "Type: uint", + RowKey.LOCATION, "Location: EDX:4", + RowKey.INTEGER, "Integer: (UNKNOWN) 0", + RowKey.VALUE, "Value: (UNKNOWN) 0h", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testReturnParameterHover() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + Function function = runToTallestRecursionAndCreateFrames(2); + HoverLocation loc = findVariableLocation(staticListing, function, ""); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: ", + RowKey.FRAME, "Frame: 0 fib pc=0040002c sp=00004ff4 base=00004ff4", + RowKey.STORAGE, "Storage: EAX:4", + RowKey.TYPE, "Type: uint", + RowKey.LOCATION, "Location: EAX:4", + RowKey.INTEGER, "Integer: (KNOWN) 1", + RowKey.VALUE, "Value: (KNOWN) 1h", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testGlobalOperandHover() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + runToRetSetGlobalAndCreateFrames(); + Instruction ins = program.getListing().getInstructionAt(globalRefInstr); + HoverLocation loc = findOperandLocation(staticListing, ins, addr(program, 0x00600000)); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: myGlobal", + RowKey.STORAGE, "Storage: 00600000:4", + RowKey.TYPE, "Type: int", + RowKey.LOCATION, "Location: 00600000:4", + RowKey.BYTES, "Bytes: (KNOWN) ef be ad de", + RowKey.INTEGER, """ + Integer: (KNOWN) 3735928559, 0xdeadbeef + -559038737, -0x21524111""", + RowKey.VALUE, "Value: (KNOWN) DEADBEEFh", + RowKey.WARNINGS, "IGNORED"), table); + } + + protected Instruction copyToDynamic(Instruction stIns) throws Throwable { + DebuggerStaticMappingService mappingService = + tool.getService(DebuggerStaticMappingService.class); + DebuggerCoordinates current = traceManager.getCurrent(); + TraceLocation dynLoc = mappingService.getOpenMappedLocation(tb.trace, + new ProgramLocation(program, stIns.getAddress()), current.getSnap()); + Address dynamicAddress = dynLoc.getAddress(); + try (UndoableTransaction tid = tb.startTransaction()) { + int length = stIns.getLength(); + assertEquals(length, tb.trace.getMemoryManager() + .putBytes(current.getSnap(), dynamicAddress, + ByteBuffer.wrap(stIns.getBytes()))); + new TraceDisassembleCommand(current.getPlatform(), dynamicAddress, + new AddressSet(dynamicAddress, dynamicAddress.add(length - 1))) + .applyToTyped(current.getView(), monitor); + } + waitForDomainObject(tb.trace); + return Objects.requireNonNull(tb.trace.getCodeManager() + .instructions() + .getAt(current.getViewSnap(), dynamicAddress)); + } + + @Test + public void testGlobalOperandInTraceHover() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + runToRetSetGlobalAndCreateFrames(); + Instruction ins = copyToDynamic(program.getListing().getInstructionAt(globalRefInstr)); + // I guess the listing needs a moment??? + HoverLocation loc = + waitForValue(() -> findOperandLocation(dynamicListing, ins, addr(program, 0x00600000))); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: myGlobal", + RowKey.STORAGE, "Storage: 00600000:4", + RowKey.TYPE, "Type: int", + RowKey.LOCATION, "Location: 00600000:4", + RowKey.BYTES, "Bytes: (KNOWN) ef be ad de", + RowKey.INTEGER, """ + Integer: (KNOWN) 3735928559, 0xdeadbeef + -559038737, -0x21524111""", + RowKey.VALUE, "Value: (KNOWN) DEADBEEFh", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testStackReferenceHover() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + runToTallestRecursionAndCreateFrames(2); + Instruction ins = program.getListing().getInstructionAt(stackRefInstr); + HoverLocation loc = findOperandLocation(staticListing, ins, new Scalar(32, 8)); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: n", + RowKey.FRAME, "Frame: 0 fib pc=0040002c sp=00004ff4 base=00004ff4", + RowKey.STORAGE, "Storage: Stack[0x4]:4", + RowKey.TYPE, "Type: uint", + RowKey.LOCATION, "Location: 00004ff8:4", + RowKey.BYTES, "Bytes: (KNOWN) 01 00 00 00", + RowKey.INTEGER, "Integer: (KNOWN) 1", + RowKey.VALUE, "Value: (KNOWN) 1h", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testRegisterReferenceHover() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + runToTallestRecursionAndCreateFrames(2); + Instruction ins = program.getListing().getInstructionAt(registerRefInstr); + HoverLocation loc = findOperandLocation(staticListing, ins, register("EDX")); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: sum", + RowKey.FRAME, "Frame: 0 fib pc=0040002c sp=00004ff4 base=00004ff4", + RowKey.STORAGE, "Storage: EDX:4", + RowKey.TYPE, "Type: uint", + RowKey.LOCATION, "Location: EDX:4", + RowKey.INTEGER, "Integer: (UNKNOWN) 0", + RowKey.VALUE, "Value: (UNKNOWN) 0h", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testSavedRegisterReferenceHover() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + // need 3 frames. 0 has already popped EBP, so not saved. 1 will save on behalf of 2. + Function function = runToTallestRecursionAndCreateFrames(3); + traceManager.activateFrame(2); + + Instruction ins = program.getListing().getInstructionAt(function.getEntryPoint()); + HoverLocation loc = findOperandLocation(staticListing, ins, register("EBP")); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: EBP", + RowKey.FRAME, "Frame: 2 fib pc=00400013 sp=00004ff8 base=00005000", + RowKey.LOCATION, "Location: 00004ff0:4", + RowKey.INTEGER, "Integer: (KNOWN) 20476, 0x4ffc", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testRegisterReferenceInTraceHover() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + runToTallestRecursionAndCreateFrames(2); + Instruction ins = copyToDynamic(program.getListing().getInstructionAt(registerRefInstr)); + // I guess the listing needs a moment??? + HoverLocation loc = + waitForValue(() -> findOperandLocation(dynamicListing, ins, register("EDX"))); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: EDX", + RowKey.INTEGER, "Integer: (UNKNOWN) 0", + RowKey.WARNINGS, "IGNORED"), table); + } + + public static HoverLocation findTokenLocation(DecompilerPanel decompilerPanel, + Function function, String tokText, String fieldText) { + DecompileResults results = waitForValue(() -> { + ProgramLocation pLoc = decompilerPanel.getCurrentLocation(); + if (!(pLoc instanceof DecompilerLocation dLoc)) { + return null; + } + DecompileResults dr = dLoc.getDecompile(); + if (dr == null || dr.getFunction() != function) { + return null; + } + return dr; + }); + + return runSwing(() -> { + Program program = function.getProgram(); + ClangLayoutController layoutController = decompilerPanel.getLayoutController(); + BigInteger numIndexes = layoutController.getNumIndexes(); + for (BigInteger i = BigInteger.ZERO; i.compareTo(numIndexes) < 0; i = + i.add(BigInteger.ONE)) { + Layout layout = layoutController.getLayout(i); + int numFields = layout.getNumFields(); + for (int j = 0; j < numFields; j++) { + Field field = layout.getField(j); + if (!(field instanceof ClangTextField clangField)) { + continue; + } + if (!fieldText.equals(field.getText())) { + continue; + } + int numRows = field.getNumRows(); + for (int r = 0; r < numRows; r++) { + int numCols = field.getNumCols(r); + for (int c = 0; c < numCols; c++) { + FieldLocation fLoc = new FieldLocation(i, j, r, c); + ClangToken token = clangField.getToken(fLoc); + if (token != null && tokText.equals(token.getText())) { + DecompilerLocation loc = token.getMinAddress() == null ? null + : new DecompilerLocation(program, token.getMinAddress(), + function.getEntryPoint(), results, token, i.intValue(), + 0); + return new HoverLocation(loc, fLoc, field, token); + } + } + } + } + } + return null; + }); + } + + protected HoverLocation findTokenLocation(Function function, String tokText, String fieldText) { + tool.showComponentProvider(decompilerProvider, true); + return findTokenLocation(decompilerPanel, function, tokText, fieldText); + } + + @Test + public void testGlobalHighVarHover() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + Function function = runToRetSetGlobalAndCreateFrames(); + HoverLocation loc = findTokenLocation(function, "myGlobal", "myGlobal = -0x21524111;"); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: myGlobal", + RowKey.STORAGE, "Storage: 00600000:4", + RowKey.TYPE, "Type: int", + RowKey.LOCATION, "Location: 00600000:4", + RowKey.BYTES, "Bytes: (KNOWN) ef be ad de", + RowKey.INTEGER, """ + Integer: (KNOWN) 3735928559, 0xdeadbeef + -559038737, -0x21524111""", + RowKey.VALUE, "Value: (KNOWN) DEADBEEFh", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testStackHighVarHover() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + Function function = runToTallestRecursionAndCreateFrames(2); + HoverLocation loc = findTokenLocation(function, "n", "if (1 < n) {"); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: n", + RowKey.FRAME, "Frame: 0 fib pc=0040002c sp=00004ff4 base=00004ff4", + RowKey.STORAGE, "Storage: Stack[0x4]:4", + RowKey.TYPE, "Type: uint", + RowKey.LOCATION, "Location: 00004ff8:4", + RowKey.BYTES, "Bytes: (KNOWN) 01 00 00 00", + RowKey.INTEGER, "Integer: (KNOWN) 1", + RowKey.VALUE, "Value: (KNOWN) 1h", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testRegisterHighVarHover() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + Function function = runToTallestRecursionAndCreateFrames(2); + // TODO: Line matching seems fragile + HoverLocation loc = findTokenLocation(function, "uVar1", "uVar1 = fib(n - 1);"); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: uVar1", + RowKey.FRAME, "Frame: 0 fib pc=0040002c sp=00004ff4 base=00004ff4", + RowKey.STORAGE, "Storage: EAX:4", + RowKey.TYPE, "Type: uint", + RowKey.LOCATION, "Location: EAX:4", + RowKey.INTEGER, "Integer: (KNOWN) 1", + RowKey.VALUE, "Value: (KNOWN) 1h", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testStructureGlobalHighVarStruct() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + Function function = runToRetFillStructAndCreateFrames(); + goTo(staticListing, new ProgramLocation(program, function.getEntryPoint())); + HoverLocation loc = + findTokenLocation(function, "myGlobal", "return (uint)myStack.y + (uint)myGlobal.m;"); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: myGlobal", + RowKey.STORAGE, "Storage: 00600000:4", + RowKey.TYPE, "Type: MyStruct", + RowKey.LOCATION, "Location: 00600000:4", + RowKey.BYTES, "Bytes: (KNOWN) e6 07 0c 09", + RowKey.INTEGER, "Integer: (KNOWN) 151783398, 0x90c07e6", + RowKey.VALUE, "Value: (KNOWN) ", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testStructureGlobalHighVarStructField() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + Function function = runToRetFillStructAndCreateFrames(); + goTo(staticListing, new ProgramLocation(program, function.getEntryPoint())); + HoverLocation loc = + findTokenLocation(function, "m", "return (uint)myStack.y + (uint)myGlobal.m;"); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: m", + RowKey.TYPE, "Type: byte", + RowKey.LOCATION, "Location: 00600002:1", + RowKey.BYTES, "Bytes: (KNOWN) 0c", + RowKey.INTEGER, "Integer: (KNOWN) 12, 0xc", + RowKey.VALUE, "Value: (KNOWN) Ch", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testStructureStackHighVarStruct() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + Function function = runToRetFillStructAndCreateFrames(); + goTo(staticListing, new ProgramLocation(program, function.getEntryPoint())); + HoverLocation loc = + findTokenLocation(function, "myStack", "return (uint)myStack.y + (uint)myGlobal.m;"); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: myStack", + RowKey.FRAME, "Frame: 1 main pc=00400012 sp=00004fe4 base=00004ff0", + RowKey.STORAGE, "Storage: Stack[-0x8]:4", + RowKey.TYPE, "Type: MyStruct", + RowKey.LOCATION, "Location: 00004fe8:4", + RowKey.BYTES, "Bytes: (UNKNOWN) 00 00 00 00", + RowKey.INTEGER, "Integer: (UNKNOWN) 0", + RowKey.VALUE, "Value: (UNKNOWN) ", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testStructureStackHighVarStructField() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + Function function = runToRetFillStructAndCreateFrames(); + goTo(staticListing, new ProgramLocation(program, function.getEntryPoint())); + HoverLocation loc = + findTokenLocation(function, "y", "return (uint)myStack.y + (uint)myGlobal.m;"); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: y", + RowKey.FRAME, "Frame: 1 main pc=00400012 sp=00004fe4 base=00004ff0", + RowKey.TYPE, "Type: word", + RowKey.LOCATION, "Location: 00004fe8:2", + RowKey.BYTES, "Bytes: (UNKNOWN) 00 00", + RowKey.INTEGER, "Integer: (UNKNOWN) 0", + RowKey.VALUE, "Value: (UNKNOWN) 0h", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testStructurePointerRegisterHighVarStruct() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + runToRetFillStructAndCreateFrames(); + Function function = program.getFunctionManager().getFunctionContaining(retInstr); + goTo(staticListing, new ProgramLocation(program, retInstr)); + HoverLocation loc = findTokenLocation(function, "s", "s->y = 0x7e6;"); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: s", + RowKey.FRAME, "Frame: 0 fillStruct pc=00400041 sp=00004fe0 base=00004fe0", + RowKey.STORAGE, "Storage: Stack[0x4]:4", + RowKey.TYPE, "Type: MyStruct *", + RowKey.LOCATION, "Location: 00004fe4:4", + // NOTE: Value is the pointer, not the struct + RowKey.BYTES, "Bytes: (KNOWN) 00 00 60 00", + RowKey.INTEGER, "Integer: (KNOWN) 6291456, 0x600000", + RowKey.VALUE, "Value: (KNOWN) 00600000", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testStructurePointerRegisterHighVarStructField() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + runToRetFillStructAndCreateFrames(); + Function function = program.getFunctionManager().getFunctionContaining(retInstr); + goTo(staticListing, new ProgramLocation(program, retInstr)); + HoverLocation loc = findTokenLocation(function, "y", "s->y = 0x7e6;"); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: y", + RowKey.FRAME, "Frame: 0 fillStruct pc=00400041 sp=00004fe0 base=00004fe0", + RowKey.TYPE, "Type: word", + RowKey.LOCATION, "Location: 00600000:2", + RowKey.BYTES, "Bytes: (KNOWN) e6 07", + RowKey.INTEGER, "Integer: (KNOWN) 2022, 0x7e6", + RowKey.VALUE, "Value: (KNOWN) 7E6h", + RowKey.WARNINGS, "IGNORED"), table); + } + + @Test + public void testArrayGlobalHighVarIndexedField() throws Throwable { + VariableValueHoverPlugin valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + VariableValueHoverService valuesService = valuesPlugin.getHoverService(); + runToRetFillStructArrayAndCreateFrames(); + Function function = program.getFunctionManager().getFunctionContaining(retInstr); + goTo(staticListing, new ProgramLocation(program, retInstr)); + HoverLocation loc = findTokenLocation(function, "m", "s[i].m = 0xc;"); + VariableValueTable table = getVariableValueTable(valuesService, loc.pLoc, + traceManager.getCurrent(), loc.fLoc, loc.field); + + assertTable(Map.of( + RowKey.NAME, "Name: m", + RowKey.FRAME, "Frame: 0 fillStruct pc=0040002e sp=00004fe0 base=00004fe0", + RowKey.TYPE, "Type: byte", + RowKey.LOCATION, "Location: 0060000a:1", + RowKey.BYTES, "Bytes: (KNOWN) 0c", + RowKey.INTEGER, "Integer: (KNOWN) 12, 0xc", + RowKey.VALUE, "Value: (KNOWN) Ch", + RowKey.WARNINGS, "IGNORED"), table); + } + + // @Test + /** + * e.g., dstack._12_4_ + */ + public void testOffcutPieceReference() throws Throwable { + Unfinished.TODO(); + } + + // @Test + public void testMultiVarnodeStorage() { + Unfinished.TODO(); + } + + // @Test + public void testWithPositiveGrowingStack() { + Unfinished.TODO(); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/AbstractCheckedTraceCachedWriteBytesPcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/AbstractCheckedTraceCachedWriteBytesPcodeExecutorStatePiece.java index b376d84577..37432fa331 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/AbstractCheckedTraceCachedWriteBytesPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/AbstractCheckedTraceCachedWriteBytesPcodeExecutorStatePiece.java @@ -15,7 +15,10 @@ */ package ghidra.pcode.exec.trace; +import java.util.Map; + import generic.ULongSpan.ULongSpanSet; +import ghidra.generic.util.datastruct.SemisparseByteArray; import ghidra.pcode.exec.trace.data.PcodeTraceDataAccess; import ghidra.program.model.address.*; import ghidra.program.model.lang.Language; @@ -38,6 +41,17 @@ public abstract class AbstractCheckedTraceCachedWriteBytesPcodeExecutorStatePiec super(language, space, backing); } + protected CheckedCachedSpace(Language language, AddressSpace space, + PcodeTraceDataAccess backing, SemisparseByteArray bytes, AddressSet written) { + super(language, space, backing, bytes, written); + } + + @Override + public CachedSpace fork() { + return new CheckedCachedSpace(language, space, backing, bytes.fork(), + new AddressSet(written)); + } + @Override public byte[] read(long offset, int size, Reason reason) { ULongSpanSet uninitialized = @@ -59,14 +73,34 @@ public abstract class AbstractCheckedTraceCachedWriteBytesPcodeExecutorStatePiec super(data); } + protected AbstractCheckedTraceCachedWriteBytesPcodeExecutorStatePiece(PcodeTraceDataAccess data, + AbstractSpaceMap spaceMap) { + super(data, spaceMap); + } + + protected class CheckedCachedSpaceMap extends TraceBackedSpaceMap { + public CheckedCachedSpaceMap() { + super(); + } + + protected CheckedCachedSpaceMap(Map spaces) { + super(spaces); + } + + @Override + protected CachedSpace newSpace(AddressSpace space, PcodeTraceDataAccess backing) { + return new CheckedCachedSpace(language, space, backing); + } + + @Override + public CheckedCachedSpaceMap fork() { + return new CheckedCachedSpaceMap(fork(spaces)); + } + } + @Override protected AbstractSpaceMap newSpaceMap() { - return new TraceBackedSpaceMap() { - @Override - protected CachedSpace newSpace(AddressSpace space, PcodeTraceDataAccess backing) { - return new CheckedCachedSpace(language, space, backing); - } - }; + return new CheckedCachedSpaceMap(); } /** diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/AddressesReadTracePcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/AddressesReadTracePcodeExecutorStatePiece.java index 415e580eb1..e06e81ee5e 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/AddressesReadTracePcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/AddressesReadTracePcodeExecutorStatePiece.java @@ -15,8 +15,7 @@ */ package ghidra.pcode.exec.trace; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import javax.help.UnsupportedOperationException; @@ -24,6 +23,7 @@ import ghidra.pcode.exec.*; import ghidra.pcode.exec.PcodeArithmetic.Purpose; import ghidra.pcode.exec.trace.data.PcodeTraceDataAccess; import ghidra.program.model.address.*; +import ghidra.program.model.lang.Register; import ghidra.program.model.mem.MemBuffer; /** @@ -43,7 +43,7 @@ public class AddressesReadTracePcodeExecutorStatePiece implements TracePcodeExecutorStatePiece { protected final PcodeTraceDataAccess data; - private final Map unique = new HashMap<>(); + private final Map unique; /** * Construct the state piece @@ -54,6 +54,15 @@ public class AddressesReadTracePcodeExecutorStatePiece super(data.getLanguage(), BytesPcodeArithmetic.forLanguage(data.getLanguage()), AddressesReadPcodeArithmetic.INSTANCE); this.data = data; + this.unique = new HashMap<>(); + } + + protected AddressesReadTracePcodeExecutorStatePiece(PcodeTraceDataAccess data, + Map unique) { + super(data.getLanguage(), BytesPcodeArithmetic.forLanguage(data.getLanguage()), + AddressesReadPcodeArithmetic.INSTANCE); + this.data = data; + this.unique = unique; } @Override @@ -66,11 +75,27 @@ public class AddressesReadTracePcodeExecutorStatePiece return data; } + @Override + public AddressesReadTracePcodeExecutorStatePiece fork() { + return new AddressesReadTracePcodeExecutorStatePiece(data, new HashMap<>(unique)); + } + @Override public void writeDown(PcodeTraceDataAccess into) { throw new UnsupportedOperationException(); } + @Override + protected Map getRegisterValuesFromSpace(AddressSpace s, + List registers) { + return Map.of(); + } + + @Override + public Map getRegisterValues() { + return Map.of(); + } + @Override protected AddressSpace getForSpace(AddressSpace space, boolean toWrite) { return space; diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/BytesTracePcodeExecutorState.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/BytesTracePcodeExecutorState.java index ecb33504be..79f29a4d01 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/BytesTracePcodeExecutorState.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/BytesTracePcodeExecutorState.java @@ -29,4 +29,13 @@ public class BytesTracePcodeExecutorState extends DefaultTracePcodeExecutorState public BytesTracePcodeExecutorState(PcodeTraceDataAccess data) { super(new BytesTracePcodeExecutorStatePiece(data)); } + + protected BytesTracePcodeExecutorState(TracePcodeExecutorStatePiece piece) { + super(piece); + } + + @Override + public BytesTracePcodeExecutorState fork() { + return new BytesTracePcodeExecutorState(piece.fork()); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/BytesTracePcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/BytesTracePcodeExecutorStatePiece.java index 1a1ec05ce7..0bd21e49ab 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/BytesTracePcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/BytesTracePcodeExecutorStatePiece.java @@ -16,11 +16,12 @@ package ghidra.pcode.exec.trace; import java.nio.ByteBuffer; +import java.util.Map; import generic.ULongSpan; import generic.ULongSpan.ULongSpanSet; -import ghidra.pcode.exec.AbstractBytesPcodeExecutorStatePiece; -import ghidra.pcode.exec.BytesPcodeExecutorStateSpace; +import ghidra.generic.util.datastruct.SemisparseByteArray; +import ghidra.pcode.exec.*; import ghidra.pcode.exec.trace.BytesTracePcodeExecutorStatePiece.CachedSpace; import ghidra.pcode.exec.trace.data.PcodeTraceDataAccess; import ghidra.program.model.address.*; @@ -41,11 +42,23 @@ public class BytesTracePcodeExecutorStatePiece protected static class CachedSpace extends BytesPcodeExecutorStateSpace { - protected final AddressSet written = new AddressSet(); + protected final AddressSet written; public CachedSpace(Language language, AddressSpace space, PcodeTraceDataAccess backing) { // Backing could be null, so we need language parameter super(language, space, backing); + this.written = new AddressSet(); + } + + protected CachedSpace(Language language, AddressSpace space, PcodeTraceDataAccess backing, + SemisparseByteArray bytes, AddressSet written) { + super(language, space, backing, bytes); + this.written = written; + } + + @Override + public CachedSpace fork() { + return new CachedSpace(language, space, backing, bytes.fork(), new AddressSet(written)); } @Override @@ -118,11 +131,22 @@ public class BytesTracePcodeExecutorStatePiece this.data = data; } + protected BytesTracePcodeExecutorStatePiece(PcodeTraceDataAccess data, + AbstractSpaceMap spaceMap) { + super(data.getLanguage(), spaceMap); + this.data = data; + } + @Override public PcodeTraceDataAccess getData() { return data; } + @Override + public BytesTracePcodeExecutorStatePiece fork() { + return new BytesTracePcodeExecutorStatePiece(data, spaceMap.fork()); + } + @Override public void writeDown(PcodeTraceDataAccess into) { if (into.getLanguage() != language) { @@ -139,6 +163,14 @@ public class BytesTracePcodeExecutorStatePiece */ protected class TraceBackedSpaceMap extends CacheingSpaceMap { + public TraceBackedSpaceMap() { + super(); + } + + protected TraceBackedSpaceMap(Map spaces) { + super(spaces); + } + @Override protected PcodeTraceDataAccess getBacking(AddressSpace space) { return data; @@ -148,6 +180,16 @@ public class BytesTracePcodeExecutorStatePiece protected CachedSpace newSpace(AddressSpace space, PcodeTraceDataAccess backing) { return new CachedSpace(language, space, backing); } + + @Override + public TraceBackedSpaceMap fork() { + return new TraceBackedSpaceMap(fork(spaces)); + } + + @Override + public CachedSpace fork(CachedSpace s) { + return s.fork(); + } } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DefaultTracePcodeExecutorState.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DefaultTracePcodeExecutorState.java index 1636d8ff68..6b2dfe9e75 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DefaultTracePcodeExecutorState.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DefaultTracePcodeExecutorState.java @@ -44,6 +44,11 @@ public class DefaultTracePcodeExecutorState extends DefaultPcodeExecutorState return piece.getData(); } + @Override + public DefaultTracePcodeExecutorState fork() { + return new DefaultTracePcodeExecutorState<>(piece.fork()); + } + @Override public void writeDown(PcodeTraceDataAccess into) { piece.writeDown(into); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DirectBytesTracePcodeExecutorState.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DirectBytesTracePcodeExecutorState.java index 276b349cb0..f77f970d48 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DirectBytesTracePcodeExecutorState.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DirectBytesTracePcodeExecutorState.java @@ -60,6 +60,11 @@ public class DirectBytesTracePcodeExecutorState extends DefaultTracePcodeExecuto super(new DirectBytesTracePcodeExecutorStatePiece(data)); } + protected DirectBytesTracePcodeExecutorState( + TracePcodeExecutorStatePiece piece) { + super(piece); + } + /** * Create the state * @@ -83,4 +88,9 @@ public class DirectBytesTracePcodeExecutorState extends DefaultTracePcodeExecuto return new PairedPcodeExecutorState<>(this, new TraceMemoryStatePcodeExecutorStatePiece(getData())); } + + @Override + public DirectBytesTracePcodeExecutorState fork() { + return new DirectBytesTracePcodeExecutorState(piece.fork()); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DirectBytesTracePcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DirectBytesTracePcodeExecutorStatePiece.java index 195e634797..7e5fa46de3 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DirectBytesTracePcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/DirectBytesTracePcodeExecutorStatePiece.java @@ -16,6 +16,8 @@ package ghidra.pcode.exec.trace; import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; import javax.help.UnsupportedOperationException; @@ -27,6 +29,7 @@ import ghidra.pcode.exec.PcodeArithmetic.Purpose; import ghidra.pcode.exec.trace.data.PcodeTraceDataAccess; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.lang.Register; import ghidra.program.model.mem.MemBuffer; import ghidra.trace.model.memory.TraceMemoryState; @@ -48,7 +51,7 @@ public class DirectBytesTracePcodeExecutorStatePiece protected final PcodeTraceDataAccess data; - protected final SemisparseByteArray unique = new SemisparseByteArray(); + protected final SemisparseByteArray unique; /** * Construct a piece @@ -57,9 +60,10 @@ public class DirectBytesTracePcodeExecutorStatePiece * @param data the trace-data access shim */ protected DirectBytesTracePcodeExecutorStatePiece(PcodeArithmetic arithmetic, - PcodeTraceDataAccess data) { + PcodeTraceDataAccess data, SemisparseByteArray unique) { super(data.getLanguage(), arithmetic, arithmetic); this.data = data; + this.unique = unique; } /** @@ -68,7 +72,7 @@ public class DirectBytesTracePcodeExecutorStatePiece * @param data the trace-data access shim */ public DirectBytesTracePcodeExecutorStatePiece(PcodeTraceDataAccess data) { - this(BytesPcodeArithmetic.forLanguage(data.getLanguage()), data); + this(BytesPcodeArithmetic.forLanguage(data.getLanguage()), data, new SemisparseByteArray()); } @Override @@ -76,6 +80,11 @@ public class DirectBytesTracePcodeExecutorStatePiece return data; } + @Override + public DirectBytesTracePcodeExecutorStatePiece fork() { + return new DirectBytesTracePcodeExecutorStatePiece(arithmetic, data, unique.fork()); + } + /** * Create a state which computes an expression's {@link TraceMemoryState} as an auxiliary * attribute @@ -129,6 +138,17 @@ public class DirectBytesTracePcodeExecutorStatePiece return buf.array(); } + @Override + protected Map getRegisterValuesFromSpace(AddressSpace s, + List registers) { + return Map.of(); + } + + @Override + public Map getRegisterValues() { + return Map.of(); + } + @Override public MemBuffer getConcreteBuffer(Address address, Purpose purpose) { throw new UnsupportedOperationException(); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/PairedTracePcodeExecutorState.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/PairedTracePcodeExecutorState.java index dad5e0b46b..8ad244b217 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/PairedTracePcodeExecutorState.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/PairedTracePcodeExecutorState.java @@ -30,30 +30,30 @@ import ghidra.pcode.exec.trace.data.PcodeTraceDataAccess; public class PairedTracePcodeExecutorState extends PairedPcodeExecutorState implements TracePcodeExecutorState> { - private final TracePcodeExecutorStatePiece left; - private final TracePcodeExecutorStatePiece right; + private final PairedTracePcodeExecutorStatePiece piece; public PairedTracePcodeExecutorState(PairedTracePcodeExecutorStatePiece piece) { super(piece); - this.left = piece.getLeft(); - this.right = piece.getRight(); + this.piece = piece; } public PairedTracePcodeExecutorState(TracePcodeExecutorState left, TracePcodeExecutorStatePiece right) { - super(left, right); - this.left = left; - this.right = right; + this(new PairedTracePcodeExecutorStatePiece<>(left, right)); } @Override public PcodeTraceDataAccess getData() { - return left.getData(); + return piece.getData(); + } + + @Override + public PairedTracePcodeExecutorState fork() { + return new PairedTracePcodeExecutorState<>(piece.fork()); } @Override public void writeDown(PcodeTraceDataAccess into) { - left.writeDown(into); - right.writeDown(into); + piece.writeDown(into); } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/PairedTracePcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/PairedTracePcodeExecutorStatePiece.java index 9b5c82b0eb..ec9fdd5aaa 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/PairedTracePcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/PairedTracePcodeExecutorStatePiece.java @@ -56,6 +56,12 @@ public class PairedTracePcodeExecutorStatePiece return left.getData(); } + @Override + public PairedTracePcodeExecutorStatePiece fork() { + return new PairedTracePcodeExecutorStatePiece<>(left.fork(), right.fork(), + getAddressArithmetic(), getArithmetic()); + } + @Override public void writeDown(PcodeTraceDataAccess into) { left.writeDown(into); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireHasKnownTraceCachedWriteBytesPcodeExecutorState.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireHasKnownTraceCachedWriteBytesPcodeExecutorState.java index 67d94d9972..c5ae94b7dc 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireHasKnownTraceCachedWriteBytesPcodeExecutorState.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireHasKnownTraceCachedWriteBytesPcodeExecutorState.java @@ -31,4 +31,14 @@ public class RequireHasKnownTraceCachedWriteBytesPcodeExecutorState public RequireHasKnownTraceCachedWriteBytesPcodeExecutorState(PcodeTraceDataAccess data) { super(new RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece(data)); } + + protected RequireHasKnownTraceCachedWriteBytesPcodeExecutorState( + TracePcodeExecutorStatePiece piece) { + super(piece); + } + + @Override + public RequireHasKnownTraceCachedWriteBytesPcodeExecutorState fork() { + return new RequireHasKnownTraceCachedWriteBytesPcodeExecutorState(piece.fork()); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java index 5b974be400..990ebbb2f2 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java @@ -40,6 +40,17 @@ public class RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece super(data); } + protected RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece(PcodeTraceDataAccess data, + AbstractSpaceMap spaceMap) { + super(data, spaceMap); + } + + @Override + public RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece fork() { + return new RequireHasKnownTraceCachedWriteBytesPcodeExecutorStatePiece(data, + spaceMap.fork()); + } + @Override protected AddressSetView getKnown(PcodeTraceDataAccess backing) { return backing.getKnownBefore(); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireIsKnownTraceCachedWriteBytesPcodeExecutorState.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireIsKnownTraceCachedWriteBytesPcodeExecutorState.java index 78c5e19731..eb42d6568f 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireIsKnownTraceCachedWriteBytesPcodeExecutorState.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireIsKnownTraceCachedWriteBytesPcodeExecutorState.java @@ -31,4 +31,14 @@ public class RequireIsKnownTraceCachedWriteBytesPcodeExecutorState public RequireIsKnownTraceCachedWriteBytesPcodeExecutorState(PcodeTraceDataAccess data) { super(new RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece(data)); } + + protected RequireIsKnownTraceCachedWriteBytesPcodeExecutorState( + TracePcodeExecutorStatePiece piece) { + super(piece); + } + + @Override + public RequireIsKnownTraceCachedWriteBytesPcodeExecutorState fork() { + return new RequireIsKnownTraceCachedWriteBytesPcodeExecutorState(piece.fork()); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java index 24b7a17c9b..38bdc460ce 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece.java @@ -16,6 +16,7 @@ package ghidra.pcode.exec.trace; import ghidra.pcode.exec.AccessPcodeExecutionException; +import ghidra.pcode.exec.PcodeExecutorStatePiece; import ghidra.pcode.exec.trace.data.PcodeTraceDataAccess; import ghidra.program.model.address.*; import ghidra.trace.model.memory.TraceMemorySpace; @@ -34,6 +35,17 @@ public class RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece super(data); } + protected RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece(PcodeTraceDataAccess data, + AbstractSpaceMap spaceMap) { + super(data, spaceMap); + } + + @Override + public RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece fork() { + return new RequireIsKnownTraceCachedWriteBytesPcodeExecutorStatePiece(data, + spaceMap.fork()); + } + /** * Construct a piece * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceMemoryStatePcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceMemoryStatePcodeExecutorStatePiece.java index 76a7ba67b1..ba60a5e2ae 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceMemoryStatePcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TraceMemoryStatePcodeExecutorStatePiece.java @@ -15,6 +15,8 @@ */ package ghidra.pcode.exec.trace; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; import generic.ULongSpan; @@ -23,6 +25,7 @@ import ghidra.pcode.exec.*; import ghidra.pcode.exec.PcodeArithmetic.Purpose; import ghidra.pcode.exec.trace.data.PcodeTraceDataAccess; import ghidra.program.model.address.*; +import ghidra.program.model.lang.Register; import ghidra.program.model.mem.MemBuffer; import ghidra.trace.model.memory.TraceMemorySpace; import ghidra.trace.model.memory.TraceMemoryState; @@ -42,7 +45,7 @@ import ghidra.trace.model.memory.TraceMemoryState; public class TraceMemoryStatePcodeExecutorStatePiece extends AbstractLongOffsetPcodeExecutorStatePiece { - protected final MutableULongSpanMap unique = new DefaultULongSpanMap<>(); + protected final MutableULongSpanMap unique; protected final PcodeTraceDataAccess data; /** @@ -55,6 +58,22 @@ public class TraceMemoryStatePcodeExecutorStatePiece extends BytesPcodeArithmetic.forLanguage(data.getLanguage()), TraceMemoryStatePcodeArithmetic.INSTANCE); this.data = data; + this.unique = new DefaultULongSpanMap<>(); + } + + protected TraceMemoryStatePcodeExecutorStatePiece(PcodeTraceDataAccess data, + MutableULongSpanMap unique) { + super(data.getLanguage(), BytesPcodeArithmetic.forLanguage(data.getLanguage()), + TraceMemoryStatePcodeArithmetic.INSTANCE); + this.data = data; + this.unique = unique; + } + + @Override + public TraceMemoryStatePcodeExecutorStatePiece fork() { + MutableULongSpanMap copyUnique = new DefaultULongSpanMap<>(); + copyUnique.putAll(unique); + return new TraceMemoryStatePcodeExecutorStatePiece(data, copyUnique); } protected AddressRange range(AddressSpace space, long offset, int size) { @@ -107,6 +126,17 @@ public class TraceMemoryStatePcodeExecutorStatePiece extends return TraceMemoryState.UNKNOWN; } + @Override + protected Map getRegisterValuesFromSpace(AddressSpace s, + List registers) { + return Map.of(); + } + + @Override + public Map getRegisterValues() { + return Map.of(); + } + @Override public MemBuffer getConcreteBuffer(Address address, Purpose purpose) { throw new ConcretionError("Cannot make TraceMemoryState into a concrete buffer", purpose); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TracePcodeExecutorState.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TracePcodeExecutorState.java index 02ac75f706..6bfe5301e3 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TracePcodeExecutorState.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TracePcodeExecutorState.java @@ -23,13 +23,14 @@ import ghidra.pcode.exec.trace.data.PcodeTraceDataAccess; * *

    * In particular, because this derives from {@link TracePcodeExecutorStatePiece}, such states are - * required to implement {@link #writeDown(PcodeTraceDataAccess)}. This interface also - * derives from {@link PcodeExecutorState} so that, as the name implies, they can be used where a - * state is required. + * required to implement {@link #writeDown(PcodeTraceDataAccess)}. This interface also derives from + * {@link PcodeExecutorState} so that, as the name implies, they can be used where a state is + * required. * * @param the type of values */ public interface TracePcodeExecutorState extends PcodeExecutorState, TracePcodeExecutorStatePiece { - // Nothing to add. Simply a composition of interfaces. + @Override + TracePcodeExecutorState fork(); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TracePcodeExecutorStatePiece.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TracePcodeExecutorStatePiece.java index 80a74e44ea..79c7de62d0 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TracePcodeExecutorStatePiece.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/pcode/exec/trace/TracePcodeExecutorStatePiece.java @@ -38,6 +38,9 @@ public interface TracePcodeExecutorStatePiece extends PcodeExecutorStatePi */ PcodeTraceDataAccess getData(); + @Override + TracePcodeExecutorStatePiece fork(); + /** * Write the accumulated values (cache) into the given trace * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/UndefinedDBTraceData.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/UndefinedDBTraceData.java index 5997f623dc..425ced0584 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/UndefinedDBTraceData.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/listing/UndefinedDBTraceData.java @@ -256,7 +256,7 @@ public class UndefinedDBTraceData implements DBTraceDataAdapter, DBTraceSpaceKey } @Override - public Data getComponentContaining(int offset) { + public TraceData getComponentContaining(int offset) { return null; } 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 e39985ecc1..b4ab4a993b 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 @@ -42,7 +42,8 @@ public interface DBTraceDelegatingManager { default void checkIsInMemory(AddressSpace space) { if (!space.isMemorySpace() && space != Address.NO_ADDRESS.getAddressSpace()) { - throw new IllegalArgumentException("Address must be in memory or NO_ADDRESS"); + throw new IllegalArgumentException( + "Address must be in memory or NO_ADDRESS. Got " + space); } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceData.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceData.java index eec5fa206a..f3317f6474 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceData.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/listing/TraceData.java @@ -29,6 +29,9 @@ public interface TraceData extends TraceCodeUnit, Data { @Override TraceData getComponentAt(int offset); + @Override + TraceData getComponentContaining(int offset); + @Override TraceData getComponent(int[] componentPath); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java index 4b4161f36f..78d3b1d8cc 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java @@ -333,6 +333,29 @@ public interface TraceMemoryOperations { Collection> getStates(long snap, AddressRange range); + /** + * Check if a range addresses are all known + * + * @param snap the time + * @param range the range to examine + * @return true if the entire range is {@link TraceMemoryState#KNOWN} + */ + default boolean isKnown(long snap, AddressRange range) { + Collection> states = getStates(snap, range); + if (states.isEmpty()) { + return false; + } + if (states.size() != 1) { + return false; + } + AddressRange entryRange = states.iterator().next().getKey().getRange(); + if (!entryRange.contains(range.getMinAddress()) || + !entryRange.contains(range.getMaxAddress())) { + return false; + } + return true; + } + /** * Break a range of addresses into smaller ranges each mapped to its most recent state at the * given time diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Scheduler.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Scheduler.java index 904f1ad2d7..b45abe10aa 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Scheduler.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Scheduler.java @@ -118,6 +118,9 @@ public interface Scheduler { TickStep slice = nextSlice(trace); eventThread = slice.getThread(tm, eventThread); emuThread = machine.getThread(eventThread.getPath(), true); + if (emuThread.getFrame() != null) { + emuThread.finishInstruction(); + } for (int i = 0; i < slice.tickCount; i++) { monitor.checkCanceled(); emuThread.stepInstruction(); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceRegisterUtils.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceRegisterUtils.java index 24fc51340a..5ac2a3bb25 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceRegisterUtils.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/TraceRegisterUtils.java @@ -200,7 +200,7 @@ public enum TraceRegisterUtils { public static void requireByteBound(Register register) { if (!isByteBound(register)) { throw new IllegalArgumentException( - "Cannot work with sub-byte registers. Consider a parent, instead."); + "Cannot work with sub-byte registers. Consider a parent instead."); } } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/ExpanderArrowExpansionVetoException.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/ExpanderArrowExpansionVetoException.java deleted file mode 100644 index 18bb60b851..0000000000 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/ExpanderArrowExpansionVetoException.java +++ /dev/null @@ -1,20 +0,0 @@ -/* ### - * 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 docking.widgets; - -public class ExpanderArrowExpansionVetoException extends Exception { - -} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/ExpanderArrowPanel.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/ExpanderArrowPanel.java deleted file mode 100644 index 633d29ea7d..0000000000 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/ExpanderArrowPanel.java +++ /dev/null @@ -1,155 +0,0 @@ -/* ### - * 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 docking.widgets; - -import java.awt.*; -import java.awt.event.*; -import java.util.concurrent.CompletableFuture; - -import javax.swing.JPanel; - -import ghidra.util.datastruct.ListenerSet; - -public class ExpanderArrowPanel extends JPanel { - // TODO: Can I make this consistent with the UI LaF - protected final static Polygon ARROW = - new Polygon(new int[] { 5, -5, -5 }, new int[] { 0, -5, 5 }, 3); - protected final static Dimension SIZE = new Dimension(16, 16); - protected final static int ANIM_MILLIS = 80; - protected final static int FRAME_MILLIS = 30; // Approx 30 fps - - private final ListenerSet listeners = - new ListenerSet<>(ExpanderArrowExpansionListener.class); - - private boolean expanded = false; - - private double animTheta; - private boolean animActive = false; - private long animTimeEnd; - private double animThetaEnd; - private double animThetaOverTimeRate; - - private final MouseListener mouseListener = new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - toggle(); - } - }; - - { - addMouseListener(mouseListener); - } - - public void addExpansionListener(ExpanderArrowExpansionListener listener) { - listeners.add(listener); - } - - public void removeExpansionListener(ExpanderArrowExpansionListener listener) { - listeners.remove(listener); - } - - protected synchronized void animateTheta(double destTheta) { - animTimeEnd = System.currentTimeMillis() + ANIM_MILLIS; - animThetaEnd = destTheta; - animThetaOverTimeRate = (destTheta - animTheta) / ANIM_MILLIS; - animActive = true; - scheduleNextFrame(); - } - - public void toggle() { - setExpanded(!expanded); - } - - protected boolean fireChanging(boolean newExpanded) { - try { - listeners.fire.changing(newExpanded); - } - catch (ExpanderArrowExpansionVetoException e) { - return false; - } - return true; - } - - protected void fireChanged() { - listeners.fire.changed(expanded); - } - - public void setExpanded(boolean expanded) { - if (this.expanded == expanded) { - return; - } - if (!fireChanging(expanded)) { - return; - } - double destTheta = expanded ? Math.PI / 2 : 0; - animateTheta(destTheta); - this.expanded = expanded; - fireChanged(); - } - - public boolean isExpanded() { - return expanded; - } - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - Graphics2D g2 = (Graphics2D) g.create(); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2.translate((double) SIZE.width / 2, (double) SIZE.height / 2); - g2.rotate(animTheta); - g2.fillPolygon(ARROW); - - if (!animActive) { - return; - } - long time = System.currentTimeMillis(); - double timeDiff = Math.max(0, animTimeEnd - time); - if (timeDiff != 0) { - double thetaDiff = timeDiff * animThetaOverTimeRate; - animTheta = animThetaEnd - thetaDiff; - scheduleNextFrame(); - return; - } - animActive = false; - if (animTheta != animThetaEnd) { - animTheta = animThetaEnd; - scheduleNextFrame(); - } - } - - @Override - public Dimension getPreferredSize() { - return SIZE; - } - - @Override - public Dimension getMinimumSize() { - return SIZE; - } - - protected void scheduleNextFrame() { - CompletableFuture.runAsync(() -> { - try { - Thread.sleep(FRAME_MILLIS); - } - catch (InterruptedException e) { - // Whatever. Render early. - } - repaint(); - }); - } -} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/generic/Span.java b/Ghidra/Debug/ProposedUtils/src/main/java/generic/Span.java index a62948b210..160a989c89 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/generic/Span.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/generic/Span.java @@ -912,7 +912,7 @@ public interface Span> extends Comparable { * @param max the upper endpoint of the span * @return the sub map */ - protected SortedMap> subMap(N min, N max) { + protected NavigableMap> subMap(N min, N max) { Entry> adjEnt = spanTree.floorEntry(min); if (adjEnt != null && adjEnt.getValue().getKey().contains(min)) { min = adjEnt.getKey(); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/generic/Unique.java b/Ghidra/Debug/ProposedUtils/src/main/java/generic/Unique.java index a8f7bb5ccc..e6dce531c3 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/generic/Unique.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/generic/Unique.java @@ -24,6 +24,16 @@ import java.util.stream.Stream; */ public interface Unique { + static T assertAtMostOne(T[] arr) { + if (arr.length == 0) { + return null; + } + if (arr.length == 1) { + return arr[0]; + } + throw new AssertionError("Expected at most one. Got many: " + List.of(arr)); + } + /** * Assert that exactly one element is in an iterable and get that element * diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/generic/util/datastruct/SemisparseByteArray.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/generic/util/datastruct/SemisparseByteArray.java index 5123ec3c0a..69e65d2636 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/generic/util/datastruct/SemisparseByteArray.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/generic/util/datastruct/SemisparseByteArray.java @@ -17,8 +17,9 @@ package ghidra.generic.util.datastruct; import java.nio.BufferOverflowException; import java.nio.BufferUnderflowException; -import java.util.HashMap; -import java.util.Map; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; import generic.ULongSpan; import generic.ULongSpan.*; @@ -62,8 +63,33 @@ public class SemisparseByteArray { /** The size of blocks used internally to store array values */ public static final int BLOCK_SIZE = 0x1000; - private final Map blocks = new HashMap<>(); - private final MutableULongSpanSet defined = new DefaultULongSpanSet(); + private final Map blocks; + private final MutableULongSpanSet defined; + + public SemisparseByteArray() { + this.blocks = new HashMap<>(); + this.defined = new DefaultULongSpanSet(); + } + + protected SemisparseByteArray(Map blocks, MutableULongSpanSet defined) { + this.blocks = blocks; + this.defined = defined; + } + + static byte[] copyArr(Map.Entry ent) { + byte[] b = ent.getValue(); + return Arrays.copyOf(b, b.length); + } + + public synchronized SemisparseByteArray fork() { + // TODO Could use some copy-on-write optimization here and in parents + Map copyBlocks = blocks.entrySet() + .stream() + .collect(Collectors.toMap(Entry::getKey, SemisparseByteArray::copyArr)); + MutableULongSpanSet copyDefined = new DefaultULongSpanSet(); + copyDefined.addAll(defined); + return new SemisparseByteArray(copyBlocks, copyDefined); + } /** * Clear the array diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/ThreadPcodeExecutorState.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/ThreadPcodeExecutorState.java index fec087c579..3499e957de 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/ThreadPcodeExecutorState.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/emu/ThreadPcodeExecutorState.java @@ -15,7 +15,7 @@ */ package ghidra.pcode.emu; -import java.util.Objects; +import java.util.*; import ghidra.pcode.exec.PcodeArithmetic; import ghidra.pcode.exec.PcodeArithmetic.Purpose; @@ -23,6 +23,7 @@ import ghidra.pcode.exec.PcodeExecutorState; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; import ghidra.program.model.mem.MemBuffer; /** @@ -62,6 +63,11 @@ public class ThreadPcodeExecutorState implements PcodeExecutorState { return arithmetic; } + @Override + public ThreadPcodeExecutorState fork() { + return new ThreadPcodeExecutorState<>(sharedState.fork(), localState.fork()); + } + /** * Decide whether or not access to the given space is directed to thread-local state * @@ -89,6 +95,14 @@ public class ThreadPcodeExecutorState implements PcodeExecutorState { return sharedState.getVar(space, offset, size, quantize, reason); } + @Override + public Map getRegisterValues() { + Map result = new HashMap<>(); + result.putAll(localState.getRegisterValues()); + result.putAll(sharedState.getRegisterValues()); + return result; + } + @Override public MemBuffer getConcreteBuffer(Address address, Purpose purpose) { assert !isThreadLocalSpace(address.getAddressSpace()); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/eval/AbstractVarnodeEvaluator.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/eval/AbstractVarnodeEvaluator.java new file mode 100644 index 0000000000..e4073741dd --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/eval/AbstractVarnodeEvaluator.java @@ -0,0 +1,430 @@ +/* ### + * 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.pcode.eval; + +import java.util.HashMap; +import java.util.Map; + +import ghidra.pcode.error.LowlevelError; +import ghidra.pcode.exec.*; +import ghidra.pcode.opbehavior.*; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.listing.Program; +import ghidra.program.model.listing.VariableStorage; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.program.model.pcode.Varnode; + +/** + * An abstract implementation of {@link VarnodeEvaluator} + * + *

    + * Unlike {@link PcodeExecutor} this abstract class is not explicitly bound to a p-code state nor + * arithmetic. Instead it defines abstract methods for accessing "leaf" varnodes and evaluating ops. + * To evaluate a varnode, it first checks if the varnode is a leaf, which is defined by an extension + * class. If it is, it converts the static address to a dynamic one and invokes the appropriate + * value getter. An extension class would likely implement those getters using a + * {@link PcodeExecutorState}. If the varnode is not a leaf, the evaluator will ascend by examining + * its defining p-code op, evaluate its input varnodes recursively and then compute the output using + * the provided p-code arithmetic. This implementation maintains a map of evaluated varnodes and + * their values so that any intermediate varnode is evaluated just once. Note that the evaluation + * algorithm assumes their are no cycles in the AST, which should be the case by definition. + * + * @param the type of values resulting from evaluation + */ +public abstract class AbstractVarnodeEvaluator implements VarnodeEvaluator { + /** + * Concatenate the given values + * + * @param sizeTotal the expected output size in bytes + * @param upper the value of the left (more significant) piece + * @param lower the value of the right (less significant) piece + * @param sizeLower the size of the lower piece + * @return the result of concatenation + */ + protected abstract T catenate(int sizeTotal, T upper, T lower, int sizeLower); + + /** + * Check if the given varnode is a leaf in the evaluation + * + *

    + * This allows the extension class to determine the base case when recursively ascending the + * AST. + * + * @param vn the varnode + * @return true to treat the varnode as a base case, or false to ascend to its defining p-code + * op + */ + protected abstract boolean isLeaf(Varnode vn); + + /** + * Resolve a (static) stack offset to its physical (dynamic) address in the frame + * + *

    + * When a leaf varnode is a stack address, this is used to map it to a physical address before + * invoking {@link #evaluateMemory(Address, int)}. + * + * @param offset the offset + * @return the address in target memory + */ + protected abstract Address applyBase(long offset); + + /** + * Map the given static address to dynamic + * + *

    + * When a leaf varnode is a memory address, this is used to map it to a dynamic address before + * invoking {@link #evaluateMemory(Address, int)}. This is needed in case the module has been + * relocated in the dynamic context. Note this is not used to translate register or stack + * addresses, since those are abstract concepts. Stack addresses are translated using + * {@link #applyBase(long)}, the result of which should already be a dynamic address. + * + * @param program the program specifying the static context + * @param address the address in the static context + * @return the address in the dynamic context + */ + protected Address translateMemory(Program program, Address address) { + return address; + } + + /** + * Evaluate a leaf varnode + * + *

    + * This method translates the varnode accordingly and delegates the evaluation, indirectly, to + * {@link #evaluateMemory(Address, int)}. Notable exceptions are constants, which are just + * evaluated to their immediate value, and unique variables, which cannot ordinarily be leaves. + * + * @param program the program defining the static context + * @param vn the varnode + * @return the value obtained from the dynamic context + */ + protected T evaluateLeaf(Program program, Varnode vn) { + Address address = vn.getAddress(); + if (address.isConstantAddress()) { + return evaluateConstant(vn.getOffset(), vn.getSize()); + } + else if (address.isRegisterAddress()) { + return evaluateRegister(address, vn.getSize()); + } + else if (address.isStackAddress()) { + return evaluateStack(address.getOffset(), vn.getSize()); + } + else if (address.isMemoryAddress()) { + return evaluateMemory(translateMemory(program, address), vn.getSize()); + } + else if (address.isUniqueAddress()) { + return evaluateUnique(vn.getOffset(), vn.getSize()); + } + else { + throw new PcodeExecutionException("Unrecognized address space in " + vn); + } + } + + /** + * Evaluate a varnode, which could be either a leaf or a branch + * + *

    + * This method is invoked by {@link #evaluateVarnode(Program, Varnode, Map)} when the value has + * not already been computed. Only that method should invoke this one directly. + * + * @param program the program defining the static context + * @param vn the varnode + * @param already a cache of already-evaluated varnodes and their values + * @return the value + */ + protected T doEvaluateVarnode(Program program, Varnode vn, Map already) { + if (isLeaf(vn)) { + return evaluateLeaf(program, vn); + } + return evaluateBranch(program, vn, already); + } + + /** + * Evaluate a varnode, which could be either a leaf or a branch, taking its cached value if + * available + * + * @param program the program defining the static context + * @param vn the varnode + * @param already a cache of already-evaluated varnodes and their values + * @return the value + */ + protected T evaluateVarnode(Program program, Varnode vn, Map already) { + // computeIfAbsent does nto work because of the recursion. Will get CME. + if (already.containsKey(vn)) { + return already.get(vn); + } + T result = doEvaluateVarnode(program, vn, already); + already.put(vn, result); + return result; + } + + /** + * Evaluate a varnode + * + * @param program the program containing the varnode + * @param vn the varnode to evaluate + * @return the value of the varnode + */ + @Override + public T evaluateVarnode(Program program, Varnode vn) { + return evaluateVarnode(program, vn, new HashMap<>()); + } + + /** + * Evaluate variable storage, providing an "identity" value + * + * @param storage the storage to evaluate + * @param identity the value if storage had no varnodes + * @return the value of the storage + */ + protected T evaluateStorage(VariableStorage storage, T identity) { + Program program = storage.getProgram(); + int total = storage.size(); + T value = identity; + for (Varnode vn : storage.getVarnodes()) { + T piece = evaluateVarnode(program, vn); + value = catenate(total, value, piece, vn.getSize()); + } + return value; + } + + /** + * Evaluate the given varnode's defining p-code op + * + * @param program the program defining the static context + * @param vn the varnode + * @param already a cache of already-evaluated varnodes and their values + * @return the value + */ + protected T evaluateBranch(Program program, Varnode vn, Map already) { + PcodeOp def = vn.getDef(); + if (def == null || def.getOutput() != vn) { + throw new PcodeExecutionException("No defining p-code op for " + vn); + } + return evaluateOp(program, def, already); + } + + /** + * Evaluate a constant + * + * @param value the constant value + * @param size the size of the value in bytes + * @return the value as a {@link T} + */ + protected abstract T evaluateConstant(long value, int size); + + /** + * Evaluate the given register variable + * + * @param address the address of the register + * @param size the size of the variable in bytes + * @return the value + */ + protected T evaluateRegister(Address address, int size) { + return evaluateMemory(address, size); + } + + /** + * Evaluate the given stack variable + * + * @param offset the stack offset of the variable + * @param size the size of the variable in bytes + * @return the value + */ + protected T evaluateStack(long offset, int size) { + return evaluateMemory(applyBase(offset), size); + } + + /** + * Evaluate a variable in memory + * + *

    + * By default all register, stack, and memory addresses are directed here. Register addresses + * are passed through without modification. Stack addresses will have the frame base applied via + * {@link #applyBase(long)}, and memory addresses will be mapped through + * {@link #translateMemory(Program, Address)}. + * + * @param address the address of the variable + * @param size the size of the variable in bytes + * @return the value + */ + protected abstract T evaluateMemory(Address address, int size); + + /** + * Evaluate a unique variable + * + *

    + * This is only invoked when trying to evaluate a leaf, which should never occur for a unique + * variable. Thus, by default, this throws a {@link PcodeExecutionException}. + * + * @param long the offset of the variable + * @param size the size of the variable in bytes + * @return the value + */ + protected T evaluateUnique(long offset, int size) { + throw new PcodeExecutionException( + String.format("Cannot evaluate unique $U%x:%d", offset, size)); + } + + /** + * Evaluate a variable whose offset is of type {@link T} + * + *

    + * The three parameters {@code space}, {@code offset}, and {@code size} imitate the varnode + * triple, except that the offset is abstract. This is typically invoked for a + * {@link PcodeOp#LOAD}, i.e., a dereference. + * + * @param program the program defining the static context + * @param space the address space of the variable + * @param offset the offset of the variable + * @param size the size of the variable in bytes + * @param already a cache of already-evaluated varnodes and their values + * @return the value + */ + protected abstract T evaluateAbstract(Program program, AddressSpace space, T offset, int size, + Map already); + + /** + * Evaluate a unary op + * + *

    + * This evaluates the input varnode then computes the output value. + * + * @param program the program defining the static context + * @param op the op whose output to evaluate + * @param unOp the concrete behavior of the op + * @param already a cache of already-evaluated varnodes and their values + * @return the output value + */ + protected abstract T evaluateUnaryOp(Program program, PcodeOp op, UnaryOpBehavior unOp, + Map already); + + /** + * Evaluate a binary op + * + *

    + * This evaluates the input varnodes then computes the output value. + * + * @param program the program defining the static context + * @param op the op whose output to evaluate + * @param binOp the concrete behavior of the op + * @param already a cache of already-evaluated varnodes and their values + * @return the output value + */ + protected abstract T evaluateBinaryOp(Program program, PcodeOp op, BinaryOpBehavior binOp, + Map already); + + /** + * Evaluate a {@link PcodeOp#PTRADD} op + * + * @param program the program defining the static context + * @param op the op whose output to evaluate + * @param already a cache of already-evaluated varnodes and their values + * @return the output value + */ + protected abstract T evaluatePtrAdd(Program program, PcodeOp op, Map already); + + /** + * Evaluate a {@link PcodeOp#PTRSUB} op + * + * @param program the program defining the static context + * @param op the op whose output to evaluate + * @param already a cache of already-evaluated varnodes and their values + * @return the output value + */ + protected abstract T evaluatePtrSub(Program program, PcodeOp op, Map already); + + /** + * Assert that a varnode is constant and get its value as an integer. + * + *

    + * Here "constant" means a literal or immediate value. It does not read from the state. + * + * @param vn the varnode + * @return the value + */ + protected int getIntConst(Varnode vn) { + if (!vn.isConstant()) { + throw new IllegalArgumentException(vn + " is not a constant"); + } + return (int) vn.getAddress().getOffset(); + } + + /** + * Evaluate a {@link PcodeOp#LOAD} op + * + * @param program the program defining the static context + * @param op the op whose output to evaluate + * @param already a cache of already-evaluated varnodes and their values + * @return the output value + */ + protected abstract T evaluateLoad(Program program, PcodeOp op, Map already); + + @Override + public T evaluateOp(Program program, PcodeOp op) { + return evaluateOp(program, op, new HashMap<>()); + } + + /** + * Like {@link #evaluateOp(Program, PcodeOp)}, but uses a cache + * + * @param program the program defining the static context + * @param op the op whose output to evaluate + * @param already a cache of already-evaluated varnodes and their values + * @return the output value + */ + protected T evaluateOp(Program program, PcodeOp op, Map already) { + OpBehavior b = OpBehaviorFactory.getOpBehavior(op.getOpcode()); + if (b == null) { + return badOp(op); + } + if (b instanceof UnaryOpBehavior unOp) { + return evaluateUnaryOp(program, op, unOp, already); + } + if (b instanceof BinaryOpBehavior binOp) { + return evaluateBinaryOp(program, op, binOp, already); + } + switch (op.getOpcode()) { + case PcodeOp.LOAD: + return evaluateLoad(program, op, already); + case PcodeOp.PTRADD: + return evaluatePtrAdd(program, op, already); + case PcodeOp.PTRSUB: + return evaluatePtrSub(program, op, already); + default: + return badOp(op); + } + } + + /** + * The method invoked when an unrecognized or unsupported operator is encountered + * + * @param op the op + * @return the value, but this usually throws an exception + */ + protected T badOp(PcodeOp op) { + switch (op.getOpcode()) { + case PcodeOp.UNIMPLEMENTED: + throw new LowlevelError( + "Encountered an unimplemented instruction at " + + op.getSeqnum().getTarget()); + default: + throw new LowlevelError( + "Unsupported p-code op at " + op.getSeqnum().getTarget() + ": " + op); + } + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/eval/ArithmeticVarnodeEvaluator.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/eval/ArithmeticVarnodeEvaluator.java new file mode 100644 index 0000000000..628e893631 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/eval/ArithmeticVarnodeEvaluator.java @@ -0,0 +1,150 @@ +/* ### + * 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.pcode.eval; + +import java.util.Map; + +import ghidra.pcode.exec.*; +import ghidra.pcode.exec.PcodeArithmetic.Purpose; +import ghidra.pcode.opbehavior.BinaryOpBehavior; +import ghidra.pcode.opbehavior.UnaryOpBehavior; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.listing.Program; +import ghidra.program.model.listing.VariableStorage; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.program.model.pcode.Varnode; + +/** + * An abstract implementation of {@link VarnodeEvaluator} that evaluates ops using a bound + * {@link PcodeArithmetic}. + * + * @param the type of values resulting from evaluation + */ +public abstract class ArithmeticVarnodeEvaluator extends AbstractVarnodeEvaluator { + /** + * A convenience for concatenating two varnodes + * + *

    + * There is no p-code op for catenation, but it is easily achieved as one might do in C or + * SLEIGH: {@code shift} the left piece then {@code or} it with the right piece. + * + * @param the type of values + * @param arithmetic the p-code arithmetic for values of type {@link T} + * @param sizeTotal the expected output size in bytes + * @param upper the value of the left (more significant) piece + * @param lower the value of the right (less significant) piece + * @param sizeLower the size of the lower piece + * @return the result of concatenation + */ + public static T catenate(PcodeArithmetic arithmetic, int sizeTotal, T upper, T lower, + int sizeLower) { + T zext = arithmetic.unaryOp(PcodeOp.INT_ZEXT, sizeTotal, sizeLower, lower); + T shift = arithmetic.binaryOp(PcodeOp.INT_LEFT, sizeTotal, sizeTotal, upper, 4, + arithmetic.fromConst(sizeLower * 8, 4)); + return arithmetic.binaryOp(PcodeOp.INT_OR, sizeTotal, sizeTotal, shift, sizeTotal, zext); + } + + private final PcodeArithmetic arithmetic; + + /** + * Construct an evaluator + * + * @param arithmetic the arithmetic for computing p-code op outputs + */ + public ArithmeticVarnodeEvaluator(PcodeArithmetic arithmetic) { + this.arithmetic = arithmetic; + } + + @Override + protected T catenate(int sizeTotal, T upper, T lower, int sizeLower) { + return catenate(arithmetic, sizeTotal, upper, lower, sizeLower); + } + + @Override + public T evaluateStorage(VariableStorage storage) { + return evaluateStorage(storage, arithmetic.fromConst(0, storage.size())); + } + + @Override + protected T evaluateConstant(long value, int size) { + return arithmetic.fromConst(value, size); + } + + @Override + protected T evaluateAbstract(Program program, AddressSpace space, T offset, int size, + Map already) { + long concrete = arithmetic.toLong(offset, Purpose.LOAD); + Address address = space.getAddress(concrete); + // There is no actual varnode to have a defining op, so this will be a leaf + return evaluateMemory(translateMemory(program, address), size); + } + + @Override + protected T evaluateUnaryOp(Program program, PcodeOp op, UnaryOpBehavior unOp, + Map already) { + Varnode in1Var = op.getInput(0); + T in1 = evaluateVarnode(program, in1Var, already); + return arithmetic.unaryOp(op, in1); + } + + @Override + protected T evaluateBinaryOp(Program program, PcodeOp op, BinaryOpBehavior binOp, + Map already) { + Varnode in1Var = op.getInput(0); + Varnode in2Var = op.getInput(1); + T in1 = evaluateVarnode(program, in1Var, already); + T in2 = evaluateVarnode(program, in2Var, already); + return arithmetic.binaryOp(op, in1, in2); + } + + @Override + protected T evaluatePtrAdd(Program program, PcodeOp op, Map already) { + Varnode baseVar = op.getInput(0); + Varnode indexVar = op.getInput(1); + int size = getIntConst(op.getInput(2)); + Varnode outVar = op.getOutput(); + T base = evaluateVarnode(program, baseVar, already); + T index = evaluateVarnode(program, indexVar, already); + return arithmetic.ptrAdd(outVar.getSize(), baseVar.getSize(), base, indexVar.getSize(), + index, size); + } + + @Override + protected T evaluatePtrSub(Program program, PcodeOp op, Map already) { + Varnode baseVar = op.getInput(0); + Varnode offsetVar = op.getInput(1); + Varnode outVar = op.getOutput(); + T base = evaluateVarnode(program, baseVar, already); + T offset = evaluateVarnode(program, offsetVar, already); + return arithmetic.ptrSub(outVar.getSize(), + baseVar.getSize(), base, + offsetVar.getSize(), offset); + } + + @Override + protected T evaluateLoad(Program program, PcodeOp op, Map already) { + int spaceID = getIntConst(op.getInput(0)); + AddressSpace space = program.getAddressFactory().getAddressSpace(spaceID); + Varnode inOffset = op.getInput(1); + T offset = evaluateVarnode(program, inOffset, already); + Varnode outVar = op.getOutput(); // Only for measuring size + T out = evaluateAbstract(program, space, offset, outVar.getSize(), already); + return arithmetic.modAfterLoad(outVar.getSize(), + inOffset.getSize(), offset, + outVar.getSize(), out); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/eval/VarnodeEvaluator.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/eval/VarnodeEvaluator.java new file mode 100644 index 0000000000..fc55df7e41 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/eval/VarnodeEvaluator.java @@ -0,0 +1,66 @@ +/* ### + * 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.pcode.eval; + +import ghidra.pcode.exec.PcodeExecutor; +import ghidra.program.model.listing.Program; +import ghidra.program.model.listing.VariableStorage; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.program.model.pcode.Varnode; + +/** + * An evaluator of high varnodes + * + *

    + * This is a limited analog to {@link PcodeExecutor} but for high p-code. It is limited in that it + * can only "execute" parts of the AST that represent expressions, as a means of evaluating them. If + * it encounters, e.g., a {@link PcodeOp#MULTIEQUAL} or phi node, it will terminate throw an + * exception. + * + * @param the type of values resulting from evaluation + */ +public interface VarnodeEvaluator { + /** + * Evaluate a varnode + * + * @param program the program containing the varnode + * @param vn the varnode to evaluate + * @return the value of the varnode + */ + T evaluateVarnode(Program program, Varnode vn); + + /** + * Evaluate variable storage + * + *

    + * Each varnode is evaluated as in {@link #evaluateStorage(VariableStorage)} and then + * concatenated. The lower-indexed varnodes in storage are the more significant pieces, similar + * to big endian. + * + * @param storage the storage + * @return the value of the storage + */ + T evaluateStorage(VariableStorage storage); + + /** + * Evaluate a high p-code op + * + * @param program the program containing the op + * @param op the p-code op + * @return the value of the op's output + */ + T evaluateOp(Program program, PcodeOp op); +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AbstractBytesPcodeExecutorStatePiece.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AbstractBytesPcodeExecutorStatePiece.java index f59267a974..398af7b215 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AbstractBytesPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AbstractBytesPcodeExecutorStatePiece.java @@ -16,10 +16,13 @@ package ghidra.pcode.exec; import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; import ghidra.program.model.mem.*; import ghidra.program.model.pcode.PcodeOp; import ghidra.util.Msg; @@ -74,7 +77,7 @@ public abstract class AbstractBytesPcodeExecutorStatePiece spaceMap = newSpaceMap(); + protected final AbstractSpaceMap spaceMap; /** * Construct a state for the given language @@ -85,6 +88,11 @@ public abstract class AbstractBytesPcodeExecutorStatePiece spaceMap) { + this(language, BytesPcodeArithmetic.forLanguage(language), spaceMap); + } + /** * Construct a state for the given language * @@ -94,6 +102,13 @@ public abstract class AbstractBytesPcodeExecutorStatePiece arithmetic) { super(language, arithmetic, arithmetic); + spaceMap = newSpaceMap(); + } + + protected AbstractBytesPcodeExecutorStatePiece(Language language, + PcodeArithmetic arithmetic, AbstractSpaceMap spaceMap) { + super(language, arithmetic, arithmetic); + this.spaceMap = spaceMap; } /** @@ -138,6 +153,11 @@ public abstract class AbstractBytesPcodeExecutorStatePiece getRegisterValuesFromSpace(S s, List registers) { + return s.getRegisterValues(registers); + } + @Override public MemBuffer getConcreteBuffer(Address address, PcodeArithmetic.Purpose purpose) { return new StateMemBuffer(address, getForSpace(address.getAddressSpace(), false)); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AbstractLongOffsetPcodeExecutorStatePiece.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AbstractLongOffsetPcodeExecutorStatePiece.java index 5d92a35447..9dda9b32ac 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AbstractLongOffsetPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AbstractLongOffsetPcodeExecutorStatePiece.java @@ -16,10 +16,13 @@ package ghidra.pcode.exec; import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; import ghidra.pcode.exec.PcodeArithmetic.Purpose; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; /** * An abstract executor state piece which internally uses {@code long} to address contents @@ -41,13 +44,48 @@ public abstract class AbstractLongOffsetPcodeExecutorStatePiece * @param the type of object for each address space */ public abstract static class AbstractSpaceMap { - protected final Map spaces = new HashMap<>(); + protected final Map spaces; + + public AbstractSpaceMap() { + this.spaces = new HashMap<>(); + } + + protected AbstractSpaceMap(Map spaces) { + this.spaces = spaces; + } public abstract S getForSpace(AddressSpace space, boolean toWrite); public Collection values() { return spaces.values(); } + + /** + * Deep copy this map, for use in a forked state (or piece) + * + * @return the copy + */ + public abstract AbstractSpaceMap fork(); + + /** + * Deep copy the given space + * + * @param s the space + * @return the copy + */ + public abstract S fork(S s); + + /** + * Produce a deep copy of the given map + * + * @param spaces the map to copy + * @return the copy + */ + public Map fork(Map spaces) { + return spaces.entrySet() + .stream() + .collect(Collectors.toMap(Entry::getKey, e -> fork(e.getValue()))); + } } /** @@ -56,6 +94,14 @@ public abstract class AbstractLongOffsetPcodeExecutorStatePiece * @param the type of object for each address space */ public abstract static class SimpleSpaceMap extends AbstractSpaceMap { + public SimpleSpaceMap() { + super(); + } + + protected SimpleSpaceMap(Map spaces) { + super(spaces); + } + /** * Construct a new space internally associated with the given address space * @@ -68,7 +114,7 @@ public abstract class AbstractLongOffsetPcodeExecutorStatePiece protected abstract S newSpace(AddressSpace space); @Override - public S getForSpace(AddressSpace space, boolean toWrite) { + public synchronized S getForSpace(AddressSpace space, boolean toWrite) { return spaces.computeIfAbsent(space, s -> newSpace(s)); } } @@ -80,6 +126,14 @@ public abstract class AbstractLongOffsetPcodeExecutorStatePiece * @param the type of cache for each address space */ public abstract static class CacheingSpaceMap extends AbstractSpaceMap { + public CacheingSpaceMap() { + super(); + } + + protected CacheingSpaceMap(Map spaces) { + super(spaces); + } + /** * Get the object backing the cache for the given address space * @@ -102,7 +156,7 @@ public abstract class AbstractLongOffsetPcodeExecutorStatePiece protected abstract S newSpace(AddressSpace space, B backing); @Override - public S getForSpace(AddressSpace space, boolean toWrite) { + public synchronized S getForSpace(AddressSpace space, boolean toWrite) { return spaces.computeIfAbsent(space, s -> newSpace(s, s.isUniqueSpace() ? null : getBacking(s))); } @@ -249,7 +303,8 @@ public abstract class AbstractLongOffsetPcodeExecutorStatePiece } @Override - public T getVar(AddressSpace space, long offset, int size, boolean quantize, Reason reason) { + public T getVar(AddressSpace space, long offset, int size, boolean quantize, + Reason reason) { checkRange(space, offset, size); if (space.isConstantSpace()) { return arithmetic.fromConst(offset, size); @@ -264,4 +319,29 @@ public abstract class AbstractLongOffsetPcodeExecutorStatePiece offset = quantizeOffset(space, offset); return getFromSpace(s, offset, size, reason); } + + /** + * Can the given space for register values, as in {@link #getRegisterValues()} + * + * @param s the space to scan + * @param registers the registers known to be in the corresponding address space + * @return the map of registers to values + */ + protected abstract Map getRegisterValuesFromSpace(S s, List registers); + + @Override + public Map getRegisterValues() { + Map> regsBySpace = language.getRegisters() + .stream() + .collect(Collectors.groupingBy(Register::getAddressSpace)); + Map result = new HashMap<>(); + for (Map.Entry> ent : regsBySpace.entrySet()) { + S s = getForSpace(ent.getKey(), false); + if (s == null) { + continue; + } + result.putAll(getRegisterValuesFromSpace(s, ent.getValue())); + } + return result; + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeArithmetic.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeArithmetic.java deleted file mode 100644 index 122ff41666..0000000000 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeArithmetic.java +++ /dev/null @@ -1,95 +0,0 @@ -/* ### - * 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.pcode.exec; - -import java.math.BigInteger; - -import ghidra.program.model.address.Address; -import ghidra.program.model.lang.Endian; - -/** - * An auxiliary arithmetic that reports the address of the control value - * - *

    - * This is intended for use as the right side of a {@link PairedPcodeArithmetic}. Note that constant - * and unique spaces are never returned. Furthermore, any computation performed on a value, - * producing a temporary value, philosophically does not exist at any address in the state. Thus, - * every operation in this arithmetic results in {@code null}. The accompanying state piece - * {@link AddressOfPcodeExecutorStatePiece} does the real "address of" logic. - */ -public enum AddressOfPcodeArithmetic implements PcodeArithmetic

    { - // NB: No temp value has a real address - /** The singleton instance */ - INSTANCE; - - @Override - public Endian getEndian() { - return null; - } - - @Override - public Address unaryOp(int opcode, int sizeout, int sizein1, Address in1) { - return null; - } - - @Override - public Address binaryOp(int opcode, int sizeout, int sizein1, Address in1, int sizein2, - Address in2) { - return null; - } - - @Override - public Address modBeforeStore(int sizeout, int sizeinAddress, Address inAddress, - int sizeinValue, Address inValue) { - return inValue; - } - - @Override - public Address modAfterLoad(int sizeout, int sizeinAddress, Address inAddress, int sizeinValue, - Address inValue) { - return inValue; - } - - @Override - public Address fromConst(byte[] value) { - return null; // TODO: Do we care about constant space? - } - - @Override - public Address fromConst(BigInteger value, int size, boolean isContextreg) { - return null; - } - - @Override - public Address fromConst(BigInteger value, int size) { - return null; - } - - @Override - public Address fromConst(long value, int size) { - return null; - } - - @Override - public byte[] toConcrete(Address value, Purpose purpose) { - throw new ConcretionError("Cannot make 'address of' concrete", purpose); - } - - @Override - public long sizeOf(Address value) { - return value.getAddressSpace().getSize() / 8; - } -} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorState.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorState.java index 5c4d73427d..f4071d2d8f 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorState.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorState.java @@ -30,4 +30,13 @@ public class BytesPcodeExecutorState extends DefaultPcodeExecutorState { super(new BytesPcodeExecutorStatePiece(language), BytesPcodeArithmetic.forLanguage(language)); } + + protected BytesPcodeExecutorState(PcodeExecutorStatePiece piece) { + super(piece); + } + + @Override + public BytesPcodeExecutorState fork() { + return new BytesPcodeExecutorState(piece.fork()); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStatePiece.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStatePiece.java index a167d36ce6..d418dd5ec9 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStatePiece.java @@ -15,6 +15,8 @@ */ package ghidra.pcode.exec; +import java.util.Map; + import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.Language; @@ -33,13 +35,43 @@ public class BytesPcodeExecutorStatePiece super(language); } + protected BytesPcodeExecutorStatePiece(Language language, + AbstractSpaceMap> spaceMap) { + super(language, spaceMap); + } + + @Override + public BytesPcodeExecutorStatePiece fork() { + return new BytesPcodeExecutorStatePiece(language, spaceMap.fork()); + } + + class BytesSpaceMap extends SimpleSpaceMap> { + BytesSpaceMap() { + super(); + } + + BytesSpaceMap(Map> spaces) { + super(spaces); + } + + @Override + protected BytesPcodeExecutorStateSpace newSpace(AddressSpace space) { + return new BytesPcodeExecutorStateSpace<>(language, space, null); + } + + @Override + public AbstractSpaceMap> fork() { + return new BytesSpaceMap(fork(spaces)); + } + + @Override + public BytesPcodeExecutorStateSpace fork(BytesPcodeExecutorStateSpace s) { + return s.fork(); + } + } + @Override protected AbstractSpaceMap> newSpaceMap() { - return new SimpleSpaceMap<>() { - @Override - protected BytesPcodeExecutorStateSpace newSpace(AddressSpace space) { - return new BytesPcodeExecutorStateSpace<>(language, space, null); - } - }; + return new BytesSpaceMap(); } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStateSpace.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStateSpace.java index 22b02f9269..33eabd48c1 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStateSpace.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/BytesPcodeExecutorStateSpace.java @@ -32,7 +32,7 @@ import ghidra.util.Msg; * @param if this space is a cache, the type of object backing this space */ public class BytesPcodeExecutorStateSpace { - protected final SemisparseByteArray bytes = new SemisparseByteArray(); + protected final SemisparseByteArray bytes; protected final Language language; // for logging diagnostics protected final AddressSpace space; protected final B backing; @@ -48,6 +48,19 @@ public class BytesPcodeExecutorStateSpace { this.language = language; this.space = space; this.backing = backing; + this.bytes = new SemisparseByteArray(); + } + + protected BytesPcodeExecutorStateSpace(Language language, AddressSpace space, B backing, + SemisparseByteArray bytes) { + this.language = language; + this.space = space; + this.backing = backing; + this.bytes = bytes; + } + + public BytesPcodeExecutorStateSpace fork() { + return new BytesPcodeExecutorStateSpace<>(language, space, backing, bytes.fork()); } /** @@ -148,6 +161,21 @@ public class BytesPcodeExecutorStateSpace { return readBytes(offset, size, reason); } + public Map getRegisterValues(List registers) { + Map result = new HashMap<>(); + for (Register reg : registers) { + long min = reg.getAddress().getOffset(); + long max = min + reg.getNumBytes(); + if (!bytes.isInitialized(min, max)) { + continue; + } + byte[] data = new byte[reg.getNumBytes()]; + bytes.getData(min, data); + result.put(reg, data); + } + return result; + } + public void clear() { bytes.clear(); } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/DefaultPcodeExecutorState.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/DefaultPcodeExecutorState.java index 9aea5e3053..3c1cf22164 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/DefaultPcodeExecutorState.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/DefaultPcodeExecutorState.java @@ -15,10 +15,13 @@ */ package ghidra.pcode.exec; +import java.util.Map; + import ghidra.pcode.exec.PcodeArithmetic.Purpose; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; import ghidra.program.model.mem.MemBuffer; /** @@ -51,6 +54,16 @@ public class DefaultPcodeExecutorState implements PcodeExecutorState { return piece.getLanguage(); } + @Override + public PcodeArithmetic getArithmetic() { + return arithmetic; + } + + @Override + public DefaultPcodeExecutorState fork() { + return new DefaultPcodeExecutorState<>(piece.fork(), arithmetic); + } + @Override public T getVar(AddressSpace space, T offset, int size, boolean quantize, Reason reason) { return piece.getVar(space, offset, size, quantize, reason); @@ -62,8 +75,8 @@ public class DefaultPcodeExecutorState implements PcodeExecutorState { } @Override - public PcodeArithmetic getArithmetic() { - return arithmetic; + public Map getRegisterValues() { + return piece.getRegisterValues(); } @Override diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/LocationPcodeArithmetic.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/LocationPcodeArithmetic.java new file mode 100644 index 0000000000..36dfb9670a --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/LocationPcodeArithmetic.java @@ -0,0 +1,123 @@ +/* ### + * 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.pcode.exec; + +import java.math.BigInteger; + +import ghidra.pcode.utils.Utils; +import ghidra.program.model.lang.Endian; +import ghidra.program.model.pcode.PcodeOp; + +/** + * An auxiliary arithmetic that reports the location the control value + * + *

    + * This is intended for use as the right side of a {@link PairedPcodeArithmetic}. Note that constant + * and unique spaces are never returned. Furthermore, any computation performed on a value, + * producing a temporary value, philosophically does not exist at any location in the state. Thus, + * most operations in this arithmetic result in {@code null}. The accompanying state piece + * {@link LocationPcodeExecutorStatePiece} generates the actual locations. + */ +public enum LocationPcodeArithmetic implements PcodeArithmetic { + BIG_ENDIAN(Endian.BIG), LITTLE_ENDIAN(Endian.LITTLE); + + public static LocationPcodeArithmetic forEndian(boolean bigEndian) { + return bigEndian ? BIG_ENDIAN : LITTLE_ENDIAN; + } + + private final Endian endian; + + private LocationPcodeArithmetic(Endian endian) { + this.endian = endian; + } + + @Override + public Endian getEndian() { + return endian; + } + + @Override + public ValueLocation unaryOp(int opcode, int sizeout, int sizein1, ValueLocation in1) { + switch (opcode) { + case PcodeOp.COPY: + case PcodeOp.INT_ZEXT: + case PcodeOp.INT_SEXT: + return in1; + default: + return null; + } + } + + @Override + public ValueLocation binaryOp(int opcode, int sizeout, int sizein1, ValueLocation in1, + int sizein2, ValueLocation in2) { + switch (opcode) { + case PcodeOp.INT_LEFT: + BigInteger amount = in2 == null ? null : in2.getConst(); + if (in2 == null) { + return null; + } + return in1.shiftLeft(amount.intValue()); + case PcodeOp.INT_OR: + return in1 == null || in2 == null ? null : in1.intOr(in2); + default: + return null; + } + } + + @Override + public ValueLocation modBeforeStore(int sizeout, int sizeinAddress, ValueLocation inAddress, + int sizeinValue, ValueLocation inValue) { + return inValue; + } + + @Override + public ValueLocation modAfterLoad(int sizeout, int sizeinAddress, ValueLocation inAddress, + int sizeinValue, ValueLocation inValue) { + return inValue; + } + + @Override + public ValueLocation fromConst(byte[] value) { + return ValueLocation.fromConst(Utils.bytesToLong(value, value.length, endian.isBigEndian()), + value.length); + } + + @Override + public ValueLocation fromConst(BigInteger value, int size, boolean isContextreg) { + return ValueLocation.fromConst(value.longValueExact(), size); + } + + @Override + public ValueLocation fromConst(BigInteger value, int size) { + return ValueLocation.fromConst(value.longValueExact(), size); + } + + @Override + public ValueLocation fromConst(long value, int size) { + return ValueLocation.fromConst(value, size); + } + + @Override + public byte[] toConcrete(ValueLocation value, Purpose purpose) { + throw new ConcretionError("Cannot make 'location' concrete", purpose); + } + + @Override + public long sizeOf(ValueLocation value) { + return value == null ? 0 : value.size(); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorStatePiece.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/LocationPcodeExecutorStatePiece.java similarity index 55% rename from Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorStatePiece.java rename to Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/LocationPcodeExecutorStatePiece.java index fccad463bd..b384252044 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/AddressOfPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/LocationPcodeExecutorStatePiece.java @@ -22,32 +22,45 @@ import ghidra.pcode.exec.PcodeArithmetic.Purpose; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; import ghidra.program.model.mem.MemBuffer; /** - * An auxiliary state piece that reports the address of the control value + * An auxiliary state piece that reports the location of the control value * *

    * This is intended for use as the right side of a {@link PairedPcodeExecutorState} or * {@link PairedPcodeExecutorStatePiece}. Except for unique spaces, sets are ignored, and gets - * simply echo back the address of the requested read. In unique spaces, the "address of" is treated - * as the value, so that values transiting unique space can correctly have their source addresses + * simply echo back the location of the requested read. In unique spaces, the "location" is treated + * as the value, so that values transiting unique space can correctly have their source locations * reported. */ -public class AddressOfPcodeExecutorStatePiece - implements PcodeExecutorStatePiece { +public class LocationPcodeExecutorStatePiece + implements PcodeExecutorStatePiece { private final Language language; + private final LocationPcodeArithmetic arithmetic; private final BytesPcodeArithmetic addressArithmetic; - private final Map unique = new HashMap<>(); + private final Map unique; /** - * Construct an "address of" state piece + * Construct a "location" state piece * * @param isBigEndian true if the control language is big endian */ - public AddressOfPcodeExecutorStatePiece(Language language) { + public LocationPcodeExecutorStatePiece(Language language) { this.language = language; - this.addressArithmetic = BytesPcodeArithmetic.forEndian(language.isBigEndian()); + boolean isBigEndian = language.isBigEndian(); + this.arithmetic = LocationPcodeArithmetic.forEndian(isBigEndian); + this.addressArithmetic = BytesPcodeArithmetic.forEndian(isBigEndian); + this.unique = new HashMap<>(); + } + + protected LocationPcodeExecutorStatePiece(Language language, + BytesPcodeArithmetic addressArithmetic, Map unique) { + this.language = language; + this.arithmetic = LocationPcodeArithmetic.forEndian(language.isBigEndian()); + this.addressArithmetic = addressArithmetic; + this.unique = unique; } @Override @@ -61,12 +74,19 @@ public class AddressOfPcodeExecutorStatePiece } @Override - public PcodeArithmetic

    getArithmetic() { - return AddressOfPcodeArithmetic.INSTANCE; + public PcodeArithmetic getArithmetic() { + return arithmetic; } @Override - public void setVar(AddressSpace space, byte[] offset, int size, boolean quantize, Address val) { + public LocationPcodeExecutorStatePiece fork() { + return new LocationPcodeExecutorStatePiece(language, addressArithmetic, + new HashMap<>(unique)); + } + + @Override + public void setVar(AddressSpace space, byte[] offset, int size, boolean quantize, + ValueLocation val) { if (!space.isUniqueSpace()) { return; } @@ -76,18 +96,23 @@ public class AddressOfPcodeExecutorStatePiece } @Override - public Address getVar(AddressSpace space, byte[] offset, int size, boolean quantize, + public ValueLocation getVar(AddressSpace space, byte[] offset, int size, boolean quantize, Reason reason) { long lOffset = addressArithmetic.toLong(offset, Purpose.LOAD); if (!space.isUniqueSpace()) { - return space.getAddress(lOffset); + return ValueLocation.fromVarnode(space.getAddress(lOffset), size); } return unique.get(lOffset); } + @Override + public Map getRegisterValues() { + return Map.of(); + } + @Override public MemBuffer getConcreteBuffer(Address address, Purpose purpose) { - throw new ConcretionError("Cannot make 'address of' concrete buffers", purpose); + throw new ConcretionError("Cannot make 'location' concrete buffers", purpose); } @Override diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorState.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorState.java index ea69019d56..104dad6375 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorState.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorState.java @@ -15,12 +15,15 @@ */ package ghidra.pcode.exec; +import java.util.Map; + import org.apache.commons.lang3.tuple.Pair; import ghidra.pcode.exec.PcodeArithmetic.Purpose; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; import ghidra.program.model.mem.MemBuffer; /** @@ -82,6 +85,16 @@ public class PairedPcodeExecutorState implements PcodeExecutorState> getRegisterValues() { + return piece.getRegisterValues(); + } + + @Override + public PairedPcodeExecutorState fork() { + return new PairedPcodeExecutorState<>(piece.fork()); + } + @Override public MemBuffer getConcreteBuffer(Address address, Purpose purpose) { return piece.getConcreteBuffer(address, purpose); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorStatePiece.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorStatePiece.java index 64ddc7bad0..6e63f83f13 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PairedPcodeExecutorStatePiece.java @@ -15,12 +15,15 @@ */ package ghidra.pcode.exec; +import java.util.*; + import org.apache.commons.lang3.tuple.Pair; import ghidra.pcode.exec.PcodeArithmetic.Purpose; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; import ghidra.program.model.mem.MemBuffer; /** @@ -88,6 +91,26 @@ public class PairedPcodeExecutorStatePiece return arithmetic; } + @Override + public Map> getRegisterValues() { + Map leftRVs = left.getRegisterValues(); + Map rightRVs = right.getRegisterValues(); + Set union = new HashSet<>(); + union.addAll(leftRVs.keySet()); + union.addAll(rightRVs.keySet()); + Map> result = new HashMap<>(); + for (Register k : union) { + result.put(k, Pair.of(leftRVs.get(k), rightRVs.get(k))); + } + return result; + } + + @Override + public PairedPcodeExecutorStatePiece fork() { + return new PairedPcodeExecutorStatePiece<>(left.fork(), right.fork(), addressArithmetic, + arithmetic); + } + @Override public void setVar(AddressSpace space, A offset, int size, boolean quantize, Pair val) { left.setVar(space, offset, size, quantize, val.getLeft()); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeArithmetic.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeArithmetic.java index 36cc91eedd..eb6aef9f5c 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeArithmetic.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeArithmetic.java @@ -160,13 +160,70 @@ public interface PcodeArithmetic { * @param op * @param in1 * @param in2 - * @return + * @return the output value */ default T binaryOp(PcodeOp op, T in1, T in2) { return binaryOp(op.getOpcode(), op.getOutput().getSize(), op.getInput(0).getSize(), in1, op.getInput(1).getSize(), in2); } + /** + * Apply the {@link PcodeOp#PTRADD} operator to the given inputs + * + *

    + * The "pointer add" op takes three operands: base, index, size; and is used as a more compact + * representation of array index address computation. The {@code size} operand must be constant. + * Suppose {@code arr} is an array whose elements are {@code size} bytes each, and the address + * of its first element is {@code base}. The decompiler would likely render the + * {@link PcodeOp#PTRADD} op as {@code &arr[index]}. An equivalent SLEIGH expression is + * {@code base + index*size}. + * + *

    + * NOTE: This op is always a result of decompiler simplification, not low p-code generation, and + * so are not ordinarily used by a {@link PcodeExecutor}. + * + * @param sizeout the size (in bytes) of the output variable + * @param sizeinBase the size (in bytes) of the variable used for the array's base address + * @param inBase the value used as the array's base address + * @param sizeinIndex the size (in bytes) of the variable used for the index + * @param inIndex the value used as the index + * @param inSize the size of each array element in bytes + * @return the output value + */ + default T ptrAdd(int sizeout, int sizeinBase, T inBase, int sizeinIndex, T inIndex, + int inSize) { + T indexSized = binaryOp(PcodeOp.INT_MULT, sizeout, + sizeinIndex, inIndex, 4, fromConst(inSize, 4)); + return binaryOp(PcodeOp.INT_ADD, sizeout, + sizeinBase, inBase, sizeout, indexSized); + } + + /** + * Apply the {@link PcodeOp#PTRSUB} operator to the given inputs + * + *

    + * The "pointer subfield" op takes two operands: base, offset; and is used as a more specific + * representation of structure field address computation. Its behavior is exactly equivalent to + * {@link PcodeOp#INT_ADD}. Suppose {@code st} is a structure pointer with a field {@code f} + * located {@link offset} bytes into the structure, and {@code st} has the value {@code base}. + * The decompiler would likely render the {@link PcodeOp#PTRSUB} op as {@code &st->f}. An + * equivalent SLEIGH expression is {@code base + offset}. + * + *

    + * NOTE: This op is always a result of decompiler simplification, not low p-code generation, and + * so are not ordinarily used by a {@link PcodeExecutor}. + * + * @param sizeout the size (in bytes) of the output variable + * @param sizeinBase the size (in bytes) of the variable used for the structure's base address + * @param inBase the value used as the structure's base address + * @param sizeinOffset the size (in bytes) of the variable used for the offset + * @param inOffset the value used as the offset + * @return the output value + */ + default T ptrSub(int sizeout, int sizeinBase, T inBase, int sizeinOffset, T inOffset) { + return binaryOp(PcodeOp.INT_ADD, sizeout, sizeinBase, inBase, sizeinOffset, inOffset); + } + /** * Apply any modifications before a value is stored * 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 cc359cc6ac..bb59ee9a30 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 @@ -215,12 +215,12 @@ public class PcodeExecutor { badOp(op); return; } - if (b instanceof UnaryOpBehavior) { - executeUnaryOp(op, (UnaryOpBehavior) b); + if (b instanceof UnaryOpBehavior unOp) { + executeUnaryOp(op, unOp); return; } - if (b instanceof BinaryOpBehavior) { - executeBinaryOp(op, (BinaryOpBehavior) b); + if (b instanceof BinaryOpBehavior binOp) { + executeBinaryOp(op, binOp); return; } switch (op.getOpcode()) { @@ -240,7 +240,7 @@ public class PcodeExecutor { executeIndirectBranch(op, frame); return; case PcodeOp.CALL: - executeCall(op, frame); + executeCall(op, frame, library); return; case PcodeOp.CALLIND: executeIndirectCall(op, frame); @@ -349,12 +349,12 @@ public class PcodeExecutor { Varnode inOffset = op.getInput(1); T offset = state.getVar(inOffset, reason); checkLoad(space, offset); - Varnode outvar = op.getOutput(); + Varnode outVar = op.getOutput(); - T out = state.getVar(space, offset, outvar.getSize(), true, reason); - T mod = arithmetic.modAfterLoad(outvar.getSize(), inOffset.getSize(), offset, - outvar.getSize(), out); - state.setVar(outvar, mod); + T out = state.getVar(space, offset, outVar.getSize(), true, reason); + T mod = arithmetic.modAfterLoad(outVar.getSize(), inOffset.getSize(), offset, + outVar.getSize(), out); + state.setVar(outVar, mod); } /** @@ -506,7 +506,7 @@ public class PcodeExecutor { * @param op the op * @param frame the frame */ - public void executeCall(PcodeOp op, PcodeFrame frame) { + public void executeCall(PcodeOp op, PcodeFrame frame, PcodeUseropLibrary library) { Address target = op.getInput(0).getAddress(); branchToOffset(arithmetic.fromConst(target.getOffset(), pointerSize), frame); branchToAddress(target); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutorState.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutorState.java index 0be2d367c0..710be426d3 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutorState.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutorState.java @@ -37,6 +37,9 @@ public interface PcodeExecutorState extends PcodeExecutorStatePiece { return getArithmetic(); } + @Override + PcodeExecutorState fork(); + /** * Use this state as the control, paired with the given auxiliary state. * diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutorStatePiece.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutorStatePiece.java index 80ff70d1f1..4f62aec502 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutorStatePiece.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeExecutorStatePiece.java @@ -15,6 +15,7 @@ */ package ghidra.pcode.exec; +import java.util.Map; import ghidra.pcode.exec.PcodeArithmetic.Purpose; import ghidra.program.model.address.*; import ghidra.program.model.lang.Language; @@ -87,6 +88,13 @@ public interface PcodeExecutorStatePiece { */ PcodeArithmetic getArithmetic(); + /** + * Create a deep copy of this state + * + * @return the copy + */ + PcodeExecutorStatePiece fork(); + /** * Set the value of a register variable * @@ -219,6 +227,16 @@ public interface PcodeExecutorStatePiece { return getVar(address.getAddressSpace(), address.getOffset(), size, quantize, reason); } + /** + * Get all register values known to this state + * + *

    + * When the state acts as a cache, it should only return those cached. + * + * @return a map of registers and their values + */ + Map getRegisterValues(); + /** * Bind a buffer of concrete bytes at the given start address * diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeProgram.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeProgram.java index 137b6291ca..0e7fe1f9d1 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeProgram.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/PcodeProgram.java @@ -22,8 +22,9 @@ import ghidra.app.plugin.processors.sleigh.template.OpTpl; import ghidra.app.util.pcode.AbstractAppender; import ghidra.app.util.pcode.AbstractPcodeFormatter; import ghidra.pcodeCPort.slghsymbol.UserOpSymbol; -import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.*; import ghidra.program.model.listing.Instruction; +import ghidra.program.model.listing.Program; import ghidra.program.model.pcode.PcodeOp; /** @@ -88,22 +89,49 @@ public class PcodeProgram { } } + /** + * Generate a p-code program from the given instruction, without overrides + * + * @param instruction the instruction + * @return the p-code program + */ + public static PcodeProgram fromInstruction(Instruction instruction) { + return fromInstruction(instruction, false); + } + /** * Generate a p-code program from the given instruction * * @param instruction the instruction - * @return the p-code program. + * @param includeOverrides as in {@link Instruction#getPcode(boolean)} + * @return the p-code program */ - public static PcodeProgram fromInstruction(Instruction instruction) { + public static PcodeProgram fromInstruction(Instruction instruction, boolean includeOverrides) { Language language = instruction.getPrototype().getLanguage(); if (!(language instanceof SleighLanguage)) { throw new IllegalArgumentException("Instruction must be parsed using Sleigh"); } - PcodeOp[] pcode = instruction.getPcode(false); + PcodeOp[] pcode = instruction.getPcode(includeOverrides); return new PcodeProgram((SleighLanguage) language, List.of(pcode), Map.of()); } + /** + * Generate a p-code program from a given program's inject library + * + * @param program the program + * @param name the name of the snippet + * @param type the type of the snippet + * @return the p-code program + */ + public static PcodeProgram fromInject(Program program, String name, int type) { + PcodeInjectLibrary library = program.getCompilerSpec().getPcodeInjectLibrary(); + InjectContext ctx = library.buildInjectContext(); + InjectPayload payload = library.getPayload(type, name); + PcodeOp[] pcode = payload.getPcode(program, ctx); + return new PcodeProgram((SleighLanguage) program.getLanguage(), List.of(pcode), Map.of()); + } + protected final SleighLanguage language; protected final List code; protected final Map useropNames = new HashMap<>(); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/ValueLocation.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/ValueLocation.java new file mode 100644 index 0000000000..caa24b7883 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/pcode/exec/ValueLocation.java @@ -0,0 +1,258 @@ +/* ### + * 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.pcode.exec; + +import java.math.BigInteger; +import java.util.*; +import java.util.stream.Collectors; + +import ghidra.program.model.address.*; +import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; +import ghidra.program.model.listing.VariableStorage; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.program.model.pcode.Varnode; + +/** + * The location of a value + * + *

    + * This is an analog to {@link VariableStorage}, except that this records the actual storage + * location of the evaluated variable or expression. This does not incorporate storage of + * intermediate dereferenced values. For example, suppose {@code R0 = 0xdeadbeef}, and we want to + * evaluate {@code *:4 R0}. The storage would be {@code ram:deadbeef:4}, not + * {@code R0,ram:deadbeef:4}. + */ +public class ValueLocation { + + private static final AddressSpace CONST = + new GenericAddressSpace("const", 64, AddressSpace.TYPE_CONSTANT, 0); + + public static String vnToString(Varnode vn, Language language) { + Register register = + language == null ? null : language.getRegister(vn.getAddress(), vn.getSize()); + if (register != null) { + return String.format("%s:%d", register.getName(), vn.getSize()); + } + return String.format("%s:%d", vn.getAddress(), vn.getSize()); + } + + private static boolean isZero(Varnode vn) { + return vn.isConstant() && vn.getOffset() == 0; + } + + private static List removeLeading0s(List nodes) { + for (int i = 0; i < nodes.size(); i++) { + if (!isZero(nodes.get(i))) { + return nodes.subList(i, nodes.size()); + } + } + return List.of(); + } + + /** + * Generate the "location" of a constant + * + * @param value the value + * @param size the size of the constant in bytes + * @return the "location" + */ + public static ValueLocation fromConst(long value, int size) { + return new ValueLocation(new Varnode(CONST.getAddress(value), size)); + } + + /** + * Generate a location from a varnode + * + * @param address the dynamic address of the variable + * @param size the size of the variable in bytes + * @return the location + */ + public static ValueLocation fromVarnode(Address address, int size) { + return new ValueLocation(new Varnode(address, size)); + } + + private final List nodes; + + /** + * Construct a location from a list of varnodes + * + *

    + * Any leading varnodes which are constant 0s are removed. + * + * @param nodes the varnodes + */ + public ValueLocation(Varnode... nodes) { + this.nodes = removeLeading0s(List.of(nodes)); + } + + /** + * Construct a location from a list of varnodes + * + *

    + * Any leading varnodes which are constant 0s are removed. + * + * @param nodes the varnodes + */ + public ValueLocation(List nodes) { + this.nodes = removeLeading0s(List.copyOf(nodes)); + } + + /** + * Get the number of varnodes for this location + * + * @return the count + */ + public int nodeCount() { + return nodes.size(); + } + + /** + * Get the address of the first varnode + * + * @return the address, or null if this location has no varnodes + */ + public Address getAddress() { + return nodes.isEmpty() ? null : nodes.get(0).getAddress(); + } + + /** + * Render this location as a string, substituting registers where applicable + * + * @param language the optional language for register substitution + * @return the string + */ + public String toString(Language language) { + return nodes.stream().map(vn -> vnToString(vn, language)).collect(Collectors.joining(",")); + } + + @Override + public String toString() { + return toString(null); + } + + /** + * Apply a {@link PcodeOp#INT_OR} operator + * + *

    + * There is a very restrictive set of constraints for which this yields a non-null location. If + * either this or that is empty, the other is returned. Otherwise, the varnodes are arranged in + * pairs by taking one from each storage starting at the right, or least-significant varnode. + * Each pair must match in length, and one of the pair must be a constant zero. The non-zero + * varnode is taken. The unpaired varnodes to the left, if any, are all taken. If any pair does + * not match in length, or if neither is zero, the resulting location is null. This logic is to + * ensure location information is accrued during concatenation. + * + * @param that the other location + * @return the location + */ + public ValueLocation intOr(ValueLocation that) { + if (this.isEmpty()) { + return that; + } + if (that.isEmpty()) { + return this; + } + ListIterator itA = this.nodes.listIterator(this.nodeCount()); + ListIterator itB = that.nodes.listIterator(that.nodeCount()); + Varnode[] result = new Varnode[Math.max(this.nodeCount(), that.nodeCount())]; + int i = result.length; + while (itA.hasNext() && itB.hasPrevious()) { + Varnode vnA = itA.previous(); + Varnode vnB = itB.previous(); + if (vnA.getSize() != vnB.getSize()) { + return null; + } + if (isZero(vnA)) { + result[--i] = vnB; + } + else if (isZero(vnB)) { + result[--i] = vnA; + } + } + while (itA.hasPrevious()) { + result[--i] = itA.previous(); + } + while (itB.hasPrevious()) { + result[--i] = itB.previous(); + } + return new ValueLocation(result); + } + + /** + * If the location represents a constant, get its value + * + * @return the constant value + */ + public BigInteger getConst() { + BigInteger result = BigInteger.ZERO; + for (Varnode vn : nodes) { + if (!vn.isConstant()) { + return null; + } + result = result.shiftLeft(vn.getSize() * 8); + result = result.or(vn.getAddress().getOffsetAsBigInteger()); + } + return result; + } + + /** + * Apply a {@link PcodeOp#INT_LEFT} operator + * + *

    + * This requires the shift amount to represent an integral number of bytes. Otherwise, the + * result is null. This simply inserts a constant zero to the right, having the number of bytes + * indicated by the shift amount. This logic is to ensure location information is accrued during + * concatenation. + * + * @param amount the number of bits to shift + * @return the location. + */ + public ValueLocation shiftLeft(int amount) { + if (amount % 8 != 0) { + return null; + } + List result = new ArrayList<>(nodes); + result.add(new Varnode(CONST.getAddress(0), amount / 8)); + return new ValueLocation(result); + } + + /** + * Get the total size of this location in bytes + * + * @return the size in bytes + */ + public int size() { + int result = 0; + for (Varnode vn : nodes) { + result += vn.getSize(); + } + return result; + } + + /** + * Check if this location includes any varnodes + * + *

    + * Note that a location cannot consist entirely of constant zeros and be non-empty. The + * constructor will have removed them all. + * + * @return true if empty + */ + public boolean isEmpty() { + return nodes.isEmpty(); + } +} diff --git a/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/AbstractTaintPcodeExecutorStatePiece.java b/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/AbstractTaintPcodeExecutorStatePiece.java index 2fe8b9e30d..d7f5adb254 100644 --- a/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/AbstractTaintPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/AbstractTaintPcodeExecutorStatePiece.java @@ -15,12 +15,16 @@ */ package ghidra.pcode.emu.taint; +import java.util.List; +import java.util.Map; + import ghidra.pcode.emu.taint.plain.TaintSpace; import ghidra.pcode.exec.*; import ghidra.pcode.exec.PcodeArithmetic.Purpose; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.Register; import ghidra.program.model.mem.MemBuffer; import ghidra.taint.model.TaintVec; @@ -116,6 +120,11 @@ public abstract class AbstractTaintPcodeExecutorStatePiece return space.get(offset, size); } + @Override + protected Map getRegisterValuesFromSpace(S space, List registers) { + return space.getRegisterValues(registers); + } + @Override public void clear() { for (S space : spaceMap.values()) { diff --git a/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/plain/TaintPcodeExecutorStatePiece.java b/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/plain/TaintPcodeExecutorStatePiece.java index dbebde4d18..89824771f0 100644 --- a/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/plain/TaintPcodeExecutorStatePiece.java +++ b/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/plain/TaintPcodeExecutorStatePiece.java @@ -71,6 +71,21 @@ public class TaintPcodeExecutorStatePiece extends AbstractTaintPcodeExecutorStat protected TaintSpace newSpace(AddressSpace space) { return new TaintSpace(); } + + @Override + public AbstractSpaceMap fork() { + throw new UnsupportedOperationException(); + } + + @Override + public TaintSpace fork(TaintSpace s) { + throw new UnsupportedOperationException(); + } }; } + + @Override + public TaintPcodeExecutorStatePiece fork() { + throw new UnsupportedOperationException(); + } } diff --git a/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/plain/TaintSpace.java b/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/plain/TaintSpace.java index fe2dc0d7b5..5637b0df6b 100644 --- a/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/plain/TaintSpace.java +++ b/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/plain/TaintSpace.java @@ -15,10 +15,10 @@ */ package ghidra.pcode.emu.taint.plain; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import ghidra.pcode.emu.taint.trace.TaintTraceSpace; +import ghidra.program.model.lang.Register; import ghidra.taint.model.TaintSet; import ghidra.taint.model.TaintVec; @@ -112,4 +112,20 @@ public class TaintSpace { public void clear() { taints.clear(); } + + public Map getRegisterValues(List registers) { + Map result = new HashMap<>(); + for (Register r : registers) { + long offset = r.getAddress().getOffset(); + TaintVec vec = new TaintVec(r.getNumBytes()); + for (int i = 0; i < vec.length; i++) { + TaintSet s = taints.get(offset + i); + if (s == null) { + continue; + } + } + result.put(r, vec); + } + return result; + } } diff --git a/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/trace/TaintTracePcodeExecutorStatePiece.java b/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/trace/TaintTracePcodeExecutorStatePiece.java index cbda94751b..44c126d26a 100644 --- a/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/trace/TaintTracePcodeExecutorStatePiece.java +++ b/Ghidra/Debug/TaintAnalysis/src/main/java/ghidra/pcode/emu/taint/trace/TaintTracePcodeExecutorStatePiece.java @@ -82,9 +82,24 @@ public class TaintTracePcodeExecutorStatePiece PcodeTracePropertyAccess backing) { return new TaintTraceSpace(space, property); } + + @Override + public AbstractSpaceMap fork() { + throw new UnsupportedOperationException(); + } + + @Override + public TaintTraceSpace fork(TaintTraceSpace s) { + throw new UnsupportedOperationException(); + } }; } + @Override + public TaintTracePcodeExecutorStatePiece fork() { + throw new UnsupportedOperationException(); + } + /** * {@inheritDoc} * diff --git a/Ghidra/Features/Decompiler/src/main/doc/pcoderef.xml b/Ghidra/Features/Decompiler/src/main/doc/pcoderef.xml index 52cd43a88b..6c1394dbb2 100644 --- a/Ghidra/Features/Decompiler/src/main/doc/pcoderef.xml +++ b/Ghidra/Features/Decompiler/src/main/doc/pcoderef.xml @@ -22,7 +22,7 @@ INT_SUB FLOAT_EQUAL - +P STORE INT_CARRY diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/AssemblyBuffer.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/AssemblyBuffer.java index ed6020307a..0fc34e3b62 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/AssemblyBuffer.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/AssemblyBuffer.java @@ -82,12 +82,54 @@ public class AssemblyBuffer { return emit(asm.assembleLine(getNext(), line)); } + /** + * Assemble a line and patch into the buffer + * + *

    + * This will not grow the buffer, so the instruction being patched must already exist in the + * buffer. The typical use case is to fix up a reference: + * + *

    +	 * AssemblyBuffer buf = new AssemblyBuffer(asm, entry);
    +	 * // ...
    +	 * Address jumpCheck = buf.getNext();
    +	 * buf.assemble("JMP 0x" + buf.getNext()); // Template must accommodate expected jump distance
    +	 * // ...
    +	 * Address labelCheck = buf.getNext();
    +	 * buf.assemble(jumpCheck, "JMP 0x" + labelCheck);
    +	 * buf.assemble("CMP ECX, 0");
    +	 * // ...
    +	 * 
    + * + *

    + * This does not check that the patched instruction matches length with the new instruction. In + * fact, the buffer does not remember instruction boundaries at all. If verification is needed, + * the caller should check the lengths of the returned byte arrays for the template and the + * patch. + * + * @param at the address of the instruction to patch + * @param line the line + * @return the resulting bytes for the assembled instruction + * @throws AssemblySyntaxException if the instruction cannot be parsed + * @throws AssemblySemanticException if the instruction cannot be encoded + * @throws IOException if the buffer cannot be written + */ + public byte[] assemble(Address at, String line) + throws AssemblySyntaxException, AssemblySemanticException, IOException { + byte[] full = baos.toByteArray(); + byte[] bytes = asm.assembleLine(at, line); + System.arraycopy(bytes, 0, full, (int) at.subtract(entry), bytes.length); + baos.reset(); + baos.write(full); + return bytes; + } + /** * Append arbitrary bytes to the buffer * * @param bytes the bytes to append * @return bytes - * @throws IOException if the bufgfer cannot be written + * @throws IOException if the buffer cannot be written */ public byte[] emit(byte[] bytes) throws IOException { baos.write(bytes); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/AssemblySelector.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/AssemblySelector.java index 88ec0ceac7..5733ba229c 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/AssemblySelector.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/AssemblySelector.java @@ -82,6 +82,7 @@ public class AssemblySelector { */ public Collection filterParse(Collection parse) throws AssemblySyntaxException { + syntaxErrors.clear(); boolean gotOne = false; for (AssemblyParseResult pr : parse) { if (pr.isError()) { @@ -97,6 +98,26 @@ public class AssemblySelector { return parse; } + protected List filterCompatibleAndSort(AssemblyResolutionResults rr, + AssemblyPatternBlock ctx) throws AssemblySemanticException { + semanticErrors.clear(); + List sorted = new ArrayList<>(); + // Select only non-erroneous results whose contexts are compatible. + for (AssemblyResolution ar : rr) { + if (ar.isError()) { + semanticErrors.add((AssemblyResolvedError) ar); + continue; + } + AssemblyResolvedPatterns rc = (AssemblyResolvedPatterns) ar; + sorted.add(rc); + } + if (sorted.isEmpty()) { + throw new AssemblySemanticException(semanticErrors); + } + sorted.sort(compareBySizeThenBits); + return sorted; + } + /** * Select an instruction from the possible results. * @@ -117,21 +138,7 @@ public class AssemblySelector { */ public AssemblyResolvedPatterns select(AssemblyResolutionResults rr, AssemblyPatternBlock ctx) throws AssemblySemanticException { - List sorted = new ArrayList<>(); - // Select only non-erroneous results whose contexts are compatible. - for (AssemblyResolution ar : rr) { - if (ar.isError()) { - semanticErrors.add((AssemblyResolvedError) ar); - continue; - } - AssemblyResolvedPatterns rc = (AssemblyResolvedPatterns) ar; - sorted.add(rc); - } - if (sorted.isEmpty()) { - throw new AssemblySemanticException(semanticErrors); - } - // Sort them - sorted.sort(compareBySizeThenBits); + List sorted = filterCompatibleAndSort(rr, ctx); // Pick just the first AssemblyResolvedPatterns res = sorted.get(0);