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 index 2a535d7b19..3e21874a4a 100644 --- 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 @@ -16,6 +16,8 @@ package ghidra.app.plugin.core.debug.gui.stack.vars; import java.awt.Window; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -34,6 +36,7 @@ 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.stack.StackUnwindWarning.CustomStackUnwindWarning; 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; @@ -147,13 +150,13 @@ public class VariableValueHoverService extends AbstractConfigurableHover private final VariableValueTable table; private final PluginTool tool; private final DebuggerCoordinates current; - private final List warnings; + private final StackUnwindWarningSet warnings; private final DebuggerStaticMappingService mappingService; private final VariableEvaluator eval; public TableFiller(VariableValueTable table, PluginTool tool, DebuggerCoordinates current, - VariableEvaluator eval, List warnings) { + VariableEvaluator eval, StackUnwindWarningSet warnings) { this.table = table; this.tool = tool; this.current = current; @@ -205,7 +208,8 @@ public class VariableValueHoverService extends AbstractConfigurableHover if (stUnit instanceof Instruction stIns) { fillDefinedData(dynData); table.add(new InstructionRow(stIns)); - table.add(new WarningsRow("Instruction taken from static listing")); + table.add(new WarningsRow( + StackUnwindWarningSet.custom("Instruction taken from static listing"))); return table; } throw new AssertionError(); @@ -304,10 +308,8 @@ public class VariableValueHoverService extends AbstractConfigurableHover } 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); - } + UnwoundFrame frame = + eval.getStackFrame(function, warnings, monitor, true); if (variable != null) { return fillFrameStorage(frame, variable.getName(), variable.getDataType(), variable.getProgram(), variable.getVariableStorage()); @@ -363,15 +365,14 @@ public class VariableValueHoverService extends AbstractConfigurableHover return executeBackground(monitor -> { UnwoundFrame frame; if (function == null) { - warnings.add("Instruction is not in a function. Using innermost frame."); + warnings.add(new CustomStackUnwindWarning( + "Instruction is not in a function. Using innermost frame.")); frame = VariableValueUtils.locateInnermost(tool, current); } else { - frame = eval.getStackFrame(function, warnings, monitor); + frame = eval.getStackFrame(function, warnings, monitor, false); } if (frame == null) { - warnings.add( - "Could not locate " + function + " in stack. Using innermost frame."); return fillRegisterNoFrame(register); } @@ -423,11 +424,8 @@ public class VariableValueHoverService extends AbstractConfigurableHover return executeBackground(monitor -> { UnwoundFrame frame = VariableValueUtils.requiresFrame(program, storage, symbolStorage) - ? eval.getStackFrame(function, warnings, monitor) + ? eval.getStackFrame(function, warnings, monitor, true) : eval.getGlobalsFakeFrame(); - if (frame == null) { - throw new UnwindException("Cannot find frame for " + function); - } return fillFrameStorage(frame, name, type, program, storage); }); } @@ -436,11 +434,8 @@ public class VariableValueHoverService extends AbstractConfigurableHover DataType type, PcodeOp op, AddressSetView symbolStorage) { return executeBackground(monitor -> { UnwoundFrame frame = VariableValueUtils.requiresFrame(op, symbolStorage) - ? eval.getStackFrame(function, warnings, monitor) + ? eval.getStackFrame(function, warnings, monitor, true) : eval.getGlobalsFakeFrame(); - if (frame == null) { - throw new UnwindException("Cannot find frame for " + function); - } return fillFrameOp(frame, function.getProgram(), name, type, op, symbolStorage); }); } @@ -578,10 +573,8 @@ public class VariableValueHoverService extends AbstractConfigurableHover 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); - } + UnwoundFrame frame = + eval.getStackFrame(function, warnings, monitor, true); return fillFrameStorage(frame, variable.getName(), variable.getDataType(), variable.getProgram(), variable.getVariableStorage()); }); @@ -590,7 +583,7 @@ public class VariableValueHoverService extends AbstractConfigurableHover public CompletableFuture fillVariableValueTable(VariableValueTable table, ProgramLocation programLocation, DebuggerCoordinates current, - FieldLocation fieldLocation, Field field, List warnings) { + FieldLocation fieldLocation, Field field, StackUnwindWarningSet warnings) { if (traceManager == null || mappingService == null || current.getPlatform() == null) { return null; } @@ -630,7 +623,7 @@ public class VariableValueHoverService extends AbstractConfigurableHover return null; } VariableValueTable table = new VariableValueTable(); - List warnings = new ArrayList<>(); + StackUnwindWarningSet warnings = new StackUnwindWarningSet(); CompletableFuture future; try { future = fillVariableValueTable(table, programLocation, @@ -638,7 +631,9 @@ public class VariableValueHoverService extends AbstractConfigurableHover } catch (Exception e) { table.add(new ErrorRow(e)); - return createTooltipComponent("" + table.toHtml()); + JComponent component = createTooltipComponent("" + table.toHtml()); + addErrorDetailsListener(component, table); + return component; } if (future == null) { return null; @@ -650,6 +645,7 @@ public class VariableValueHoverService extends AbstractConfigurableHover if (!(component instanceof JToolTip tooltip)) { throw new AssertionError("Expected a JToolTip"); } + addErrorDetailsListener(component, table); future.handleAsync((__, ex) -> { table.remove(RowKey.STATUS); if (ex != null) { @@ -668,6 +664,27 @@ public class VariableValueHoverService extends AbstractConfigurableHover return tooltip; } + protected void addErrorDetailsListener(JComponent component, VariableValueTable table) { + component.addMouseListener(new MouseAdapter() { + private boolean isShiftDoubleClick(MouseEvent evt) { + if (evt.getClickCount() != 2) { + return false; + } + if ((evt.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) == 0) { + return false; + } + return true; + } + + @Override + public void mouseClicked(MouseEvent evt) { + if (isShiftDoubleClick(evt)) { + table.reportDetails(); + } + } + }); + } + 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 index 278eb97f2a..3a7ae1d76a 100644 --- 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 @@ -18,13 +18,11 @@ package ghidra.app.plugin.core.debug.gui.stack.vars; import static ghidra.app.plugin.core.debug.gui.stack.vars.VariableValueRow.*; import java.nio.ByteBuffer; -import java.util.List; import java.util.stream.Collectors; -import java.util.stream.Stream; import generic.theme.GColor; import generic.theme.GThemeDefaults.Colors; -import ghidra.app.plugin.core.debug.stack.UnwoundFrame; +import ghidra.app.plugin.core.debug.stack.*; import ghidra.pcode.exec.DebuggerPcodeUtils.PrettyBytes; import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValue; import ghidra.pcode.exec.ValueLocation; @@ -40,6 +38,7 @@ import ghidra.trace.model.listing.TraceCodeUnit; import ghidra.trace.model.memory.*; import ghidra.trace.util.TraceAddressSpace; import ghidra.util.HTMLUtilities; +import ghidra.util.Msg; import ghidra.util.exception.InvalidInputException; /** @@ -61,7 +60,8 @@ public interface VariableValueRow { * @return the HTML-styled string */ static String styleSimple(Object obj) { - return obj == null ? htmlFg(COLOR_ERROR, "None") : HTMLUtilities.escapeHTML(obj.toString()); + return obj == null ? htmlFg(COLOR_ERROR, "None") + : HTMLUtilities.escapeHTML(obj.toString()).replace("\n", "
"); } /** @@ -566,19 +566,7 @@ public interface VariableValueRow { * 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"))); - } - + record WarningsRow(StackUnwindWarningSet warnings) implements VariableValueRow { @Override public RowKey key() { return RowKey.WARNINGS; @@ -591,8 +579,8 @@ public interface VariableValueRow { @Override public String valueToHtml() { - String[] split = warnings.split("\n"); - String formatted = Stream.of(split) + String formatted = warnings.summarize() + .stream() .map(w -> String.format("
  • %s
  • ", HTMLUtilities.escapeHTML(w))) .collect(Collectors.joining("\n ")); return String.format(""" @@ -604,17 +592,22 @@ public interface VariableValueRow { @Override public String valueToSimpleString() { - return warnings; + return warnings.summarize().stream().collect(Collectors.joining("\n")); } @Override public String toHtml() { - if (warnings.isBlank()) { + if (warnings.isEmpty()) { return ""; } return String.format("%s%s", keyToHtml(), valueToHtml()); } + + @Override + public void reportDetails() { + warnings.reportDetails(); + } } /** @@ -633,11 +626,17 @@ public interface VariableValueRow { @Override public String valueToHtml() { + if (error instanceof EvaluationException || error instanceof UnwindException) { + return styleSimple(error.getMessage()); + } return styleSimple(error); } @Override public String valueToSimpleString() { + if (error instanceof EvaluationException || error instanceof UnwindException) { + return error.getMessage(); + } return error.toString(); } @@ -646,5 +645,13 @@ public interface VariableValueRow { return String.format("%s%s", keyToHtml(), valueToHtml()); } + + @Override + public void reportDetails() { + Msg.showError(this, null, "Details", error.getMessage(), error); + } + } + + default void reportDetails() { } } 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 index d982127d62..e7929ac366 100644 --- 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 @@ -112,4 +112,12 @@ public class VariableValueTable { rows.remove(key); } } + + public void reportDetails() { + synchronized (rows) { + for (VariableValueRow row : rows.values()) { + row.reportDetails(); + } + } + } } 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 index 57f67c1387..14ebf70608 100644 --- 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 @@ -21,6 +21,7 @@ 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.app.plugin.core.debug.stack.StackUnwindWarning.CustomStackUnwindWarning; import ghidra.docking.settings.Settings; import ghidra.docking.settings.SettingsDefinition; import ghidra.framework.plugintool.PluginTool; @@ -71,6 +72,9 @@ public enum VariableValueUtils { @Override protected boolean isLeaf(Varnode vn) { + if (vn.getDef() == null && (vn.isRegister() || vn.isAddress())) { + return true; + } return vn.isConstant() || symbolStorage.contains(vn.getAddress(), vn.getAddress().add(vn.getSize() - 1)); } @@ -516,7 +520,7 @@ public enum VariableValueUtils { * 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 + * It's not the greatest, but any 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 @@ -528,16 +532,24 @@ public enum VariableValueUtils { public static AddressSet collectSymbolStorage(ClangLine line) { AddressSet storage = new AddressSet(); for (ClangToken tok : line.getAllTokens()) { + Varnode vn = tok.getVarnode(); + if (vn != null) { + storage.add(rangeFromVarnode(vn)); + } HighVariable hVar = tok.getHighVariable(); if (hVar == null) { continue; } + Varnode rep = hVar.getRepresentative(); + if (rep != null) { + storage.add(rangeFromVarnode(rep)); + } HighSymbol hSym = hVar.getSymbol(); if (hSym == null) { continue; } - for (Varnode vn : hSym.getStorage().getVarnodes()) { - storage.add(rangeFromVarnode(vn)); + for (Varnode stVn : hSym.getStorage().getVarnodes()) { + storage.add(rangeFromVarnode(stVn)); } } return storage; @@ -725,25 +737,54 @@ public enum VariableValueUtils { * @param function the desired function * @param warnings a place to emit warnings * @param monitor a monitor for cancellation + * @param required whether to throw an exception or register a warning * @return the frame if found, or null */ - public UnwoundFrame getStackFrame(Function function, List warnings, - TaskMonitor monitor) { + public UnwoundFrame getStackFrame(Function function, + StackUnwindWarningSet warnings, TaskMonitor monitor, boolean required) { synchronized (lock) { if (unwound == null) { - doUnwind(monitor); + try { + doUnwind(monitor); + } + catch (Exception e) { + /** + * Most exceptions should be caught and wrapped by the unwind analysis. If + * one gets here, something bad has happened, and for debugging purposes, we + * should invalidate, so that the error will repeat next time the frame is + * requested. + */ + unwound = null; + throw e; + } } 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); + StackUnwindWarningSet unwindWarnings = frame.getWarnings(); + if (unwindWarnings != null) { + warnings.addAll(unwindWarnings); } return frame; } } + String message; + if (unwound.isEmpty()) { + message = "Could not recover the innermost frame!"; + } + else { + message = "There is no frame for %s among the %d frames unwound." + .formatted(function, unwound.size()); + Exception error = unwound.get(unwound.size() - 1).getError(); + if (error != null) { + message += "\nTerminating error: %s".formatted(error.getMessage()); + } + } + if (required) { + throw new UnwindException(message); + } + warnings.add(new CustomStackUnwindWarning(message)); return null; } } 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 index 3579f87e22..be12002b89 100644 --- 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 @@ -78,7 +78,10 @@ public abstract class AbstractUnwoundFrame implements UnwoundFrame { protected Address translateMemory(Program program, Address address) { TraceLocation location = mappingService.getOpenMappedLocation(trace, new ProgramLocation(program, address), snap); - return location == null ? null : location.getAddress(); + if (location == null) { + throw new DynamicMappingException(program, address); + } + return location.getAddress(); } } @@ -97,7 +100,10 @@ public abstract class AbstractUnwoundFrame implements UnwoundFrame { protected Address translateMemory(Program program, Address address) { TraceLocation location = mappingService.getOpenMappedLocation(trace, new ProgramLocation(program, address), snap); - return location == null ? null : location.getAddress(); + if (location == null) { + throw new DynamicMappingException(program, address); + } + return location.getAddress(); } } @@ -131,6 +137,9 @@ public abstract class AbstractUnwoundFrame implements UnwoundFrame { @Override protected boolean isLeaf(Varnode vn) { + if (vn.getDef() == null && (vn.isRegister() || vn.isAddress())) { + return true; + } return vn.isConstant() || symbolStorage.contains(vn.getAddress(), vn.getAddress().add(vn.getSize() - 1)); } 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 index a0cbc27b4e..1c583fd6d7 100644 --- 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 @@ -18,6 +18,7 @@ package ghidra.app.plugin.core.debug.stack; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import ghidra.app.plugin.core.bookmark.BookmarkNavigator; import ghidra.app.plugin.core.debug.DebuggerCoordinates; @@ -61,8 +62,8 @@ public class AnalysisUnwoundFrame extends AbstractUnwoundFrame { private final int level; private final Address pcVal; private final Address spVal; + private final Address staticPcVal; private final UnwindInfo info; - private final UnwindException infoErr; private final SavedRegisterMap registerMap; private final Address base; @@ -83,35 +84,29 @@ public class AnalysisUnwoundFrame extends AbstractUnwoundFrame { * 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 pcVal the (dynamic) 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 staticPcVal the (static) address of the next instruction * @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) { + PcodeExecutorState state, int level, Address pcVal, Address spVal, + Address staticPcVal, UnwindInfo info, 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.staticPcVal = staticPcVal; this.info = info; - this.infoErr = infoErr; this.registerMap = registerMap; - if (info != null) { - this.base = info.computeBase(spVal); - } - else { - this.base = null; - } + this.base = info.computeBase(spVal); } @Override @@ -156,8 +151,10 @@ public class AnalysisUnwoundFrame extends AbstractUnwoundFrame { @Override protected Address applyBase(long offset) { if (base == null) { - throw new UnwindException("Cannot compute stack address for offset " + offset, - infoErr); + throw new UnwindException( + "Cannot compute stack address for offset %d.\nFrame error: %s".formatted(offset, + info.error().getMessage()), + info.error()); } return base.add(offset); } @@ -194,8 +191,11 @@ public class AnalysisUnwoundFrame extends AbstractUnwoundFrame { @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)); + return String.format("%s %s pc=%s sp=%s base=%s", + level, info.function(), + pcVal == null ? null : pcVal.toString(false), + spVal == null ? null : spVal.toString(false), + base == null ? null : base.toString(false)); } @Override @@ -225,7 +225,7 @@ public class AnalysisUnwoundFrame extends AbstractUnwoundFrame { */ protected Structure generateStructure(int prevParamSize) { FrameStructureBuilder builder = - new FrameStructureBuilder(language, pcVal, info, prevParamSize); + new FrameStructureBuilder(language, staticPcVal, info, prevParamSize); return builder.build(StackUnwinder.FRAMES_PATH, "frame_" + pcVal.toString(false), trace.getDataTypeManager()); } @@ -324,7 +324,7 @@ public class AnalysisUnwoundFrame extends AbstractUnwoundFrame { TraceBookmarkManager bm = trace.getBookmarkManager(); TraceBookmarkType btWarn = getWarningBookmarkType(); Lifespan span = Lifespan.nowOnMaybeScratch(viewSnap); - String warnings = info.warnings().summarize(); + String warnings = info.warnings().summarize().stream().collect(Collectors.joining("\n")); Structure structure = resolveStructure(prevParamSize); if (structure == null || structure.isZeroLength()) { for (TraceBookmark existing : bm.getBookmarksAt(viewSnap, spPlusParams)) { @@ -374,27 +374,13 @@ public class AnalysisUnwoundFrame extends AbstractUnwoundFrame { 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 StackUnwindWarningSet getWarnings() { + return info.warnings(); } @Override - public String getWarnings() { - if (info == null) { - return ""; - } - return info.warnings().summarize(); + public Exception getError() { + return info.error(); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/DynamicMappingException.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/DynamicMappingException.java new file mode 100644 index 0000000000..0abc3e4583 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/DynamicMappingException.java @@ -0,0 +1,38 @@ +/* ### + * 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.listing.Program; + +public class DynamicMappingException extends EvaluationException { + private final Program program; + private final Address address; + + public DynamicMappingException(Program program, Address address) { + super("Cannot map %s:%s to dynamic adress".formatted(program.getName(), address)); + this.program = program; + this.address = address; + } + + public Program getProgram() { + return program; + } + + public Address getAddress() { + return address; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/EvaluationException.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/EvaluationException.java new file mode 100644 index 0000000000..7519ba789e --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/EvaluationException.java @@ -0,0 +1,22 @@ +/* ### + * 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; + +public class EvaluationException extends RuntimeException { + public EvaluationException(String message) { + super(message); + } +} 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 index 52bcf74c74..89d09b4f1d 100644 --- 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 @@ -87,8 +87,13 @@ public class FakeUnwoundFrame extends AbstractUnwoundFrame { } @Override - public String getWarnings() { - return ""; + public StackUnwindWarningSet getWarnings() { + return new StackUnwindWarningSet(); + } + + @Override + public Exception getError() { + return null; } @Override 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 index 51f8b06055..a4f341dccf 100644 --- 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 @@ -16,6 +16,7 @@ package ghidra.app.plugin.core.debug.stack; import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.stack.StackUnwindWarning.CustomStackUnwindWarning; import ghidra.framework.plugintool.PluginTool; import ghidra.pcode.exec.DebuggerPcodeUtils; import ghidra.pcode.exec.DebuggerPcodeUtils.WatchValue; @@ -263,14 +264,28 @@ public class ListingUnwoundFrame extends AbstractUnwoundFrame { } @Override - public String getWarnings() { + public StackUnwindWarningSet getWarnings() { + StackUnwindWarningSet warnings = new StackUnwindWarningSet(); for (TraceBookmark bookmark : frame.getTrace() .getBookmarkManager() .getBookmarksAt(frame.getStartSnap(), frame.getMinAddress())) { - if (bookmark.getTypeString().equals(BookmarkType.WARNING)) { - return bookmark.getComment(); + if (!bookmark.getTypeString().equals(BookmarkType.WARNING)) { + continue; + } + String comment = bookmark.getComment(); + if (comment == null) { + continue; + } + for (String line : comment.split("\n")) { + warnings.add(new CustomStackUnwindWarning(line)); } } + return warnings; + } + + @Override + public Exception getError() { + // TODO: Can this be deserialized from a bookmark? return null; } 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 index a0c2eba094..5aef0e094c 100644 --- 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 @@ -23,9 +23,8 @@ 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; -import ghidra.program.model.pcode.VarnodeAST; +import ghidra.program.model.pcode.*; +import ghidra.util.Msg; /** * A warning issued while unwinding a stack @@ -67,6 +66,12 @@ public interface StackUnwindWarning { return false; } + /** + * For diagnostics, report any error details indicated by this warning, usually via {@link Msg}. + */ + default void reportDetails() { + } + /** * The unwind analyzer could not find an exit path from the frame's program counter. */ @@ -85,10 +90,17 @@ public interface StackUnwindWarning { /** * The unwind analyzer discovered at last one exit path, but none could be analyzed. */ - public record OpaqueReturnPathStackUnwindWarning(Address pc) implements StackUnwindWarning { + public record OpaqueReturnPathStackUnwindWarning(Address pc, Exception last) + implements StackUnwindWarning { @Override public String getMessage() { - return "Could not analyze any path from " + pc + " to a return"; + return "Could not analyze any path from " + pc + " to a return.\nLast error: " + + last.getMessage(); + } + + @Override + public void reportDetails() { + Msg.showError(this, null, "Details", getMessage(), last); } } @@ -188,7 +200,8 @@ public interface StackUnwindWarning { } /** - * While analyzing an indirect call, couldn't get the function signature because its input doesn't have a high variable. + * While analyzing an indirect call, couldn't get the function signature because its input + * doesn't have a high variable. */ public record NoHighVariableFromTargetPointerTypeUnwindWarning(VarnodeAST vn) implements StackUnwindWarning { @@ -208,4 +221,15 @@ public interface StackUnwindWarning { return "Could not recover signature of indirect call: " + op; } } + + /** + * A custom warning, either because a specific type is too onerous, or because the message was + * deserialized and the specific type and info cannot be recovered. + */ + public record CustomStackUnwindWarning(String message) implements StackUnwindWarning { + @Override + public String getMessage() { + return message; + } + } } 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 index 3d3a652cd7..5c5c01226a 100644 --- 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 @@ -19,6 +19,7 @@ import java.util.*; import java.util.stream.Collectors; import ghidra.app.plugin.core.debug.stack.StackUnwindWarning.Combinable; +import ghidra.app.plugin.core.debug.stack.StackUnwindWarning.CustomStackUnwindWarning; /** * A bucket of warnings @@ -29,6 +30,12 @@ import ghidra.app.plugin.core.debug.stack.StackUnwindWarning.Combinable; public class StackUnwindWarningSet implements Collection { private final Collection warnings = new LinkedHashSet<>(); + public static StackUnwindWarningSet custom(String message) { + StackUnwindWarningSet set = new StackUnwindWarningSet(); + set.add(new CustomStackUnwindWarning(message)); + return set; + } + /** * Create a new empty set */ @@ -130,7 +137,7 @@ public class StackUnwindWarningSet implements Collection { } @SuppressWarnings({ "rawtypes", "unchecked" }) - public String summarize() { + public List summarize() { Set> combined = new LinkedHashSet<>(); List lines = new ArrayList<>(); for (StackUnwindWarning w : warnings) { @@ -155,6 +162,12 @@ public class StackUnwindWarningSet implements Collection { lines.add(w.getMessage()); } } - return lines.stream().collect(Collectors.joining("\n")); + return lines; + } + + public void reportDetails() { + for (StackUnwindWarning w : warnings) { + w.reportDetails(); + } } } 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 index ccfc5a81b2..e7508e1a40 100644 --- 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 @@ -203,6 +203,9 @@ public class StackUnwinder { return unwind(coordinates, level, pcVal, spVal, state, new SavedRegisterMap(), monitor); } + record StaticAndUnwind(Address staticPc, UnwindInfo info) { + } + /** * Compute the unwind information for the given program counter and context * @@ -217,8 +220,8 @@ public class StackUnwinder { * @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 { + public StaticAndUnwind computeUnwindInfo(long snap, int level, Address pcVal, + TaskMonitor monitor) throws CancelledException { // TODO: Try markup in trace first? ProgramLocation staticPcLoc = mappings == null ? null : mappings.getOpenMappedLocation( @@ -229,23 +232,27 @@ public class StackUnwinder { } Program program = staticPcLoc.getProgram(); Address staticPc = staticPcLoc.getAddress(); - // TODO: Cache these? - UnwindAnalysis ua = new UnwindAnalysis(program); - return ua.computeUnwindInfo(staticPc, monitor); + try { + // TODO: Cache these? + UnwindAnalysis ua = new UnwindAnalysis(program); + return new StaticAndUnwind(staticPc, ua.computeUnwindInfo(staticPc, monitor)); + } + catch (Exception e) { + return new StaticAndUnwind(staticPc, UnwindInfo.errorOnly(e)); + } } AnalysisUnwoundFrame unwind(DebuggerCoordinates coordinates, int level, Address pcVal, Address spVal, PcodeExecutorState state, SavedRegisterMap registerMap, - TaskMonitor monitor) - throws CancelledException { + TaskMonitor monitor) throws CancelledException { try { - UnwindInfo info = computeUnwindInfo(coordinates.getSnap(), level, pcVal, monitor); + StaticAndUnwind sau = computeUnwindInfo(coordinates.getSnap(), level, pcVal, monitor); return new AnalysisUnwoundFrame<>(tool, coordinates, this, state, level, pcVal, spVal, - info, null, registerMap); + sau.staticPc, sau.info, registerMap); } - catch (UnwindException e) { + catch (Exception e) { return new AnalysisUnwoundFrame<>(tool, coordinates, this, state, level, pcVal, spVal, - null, e, registerMap); + null, UnwindInfo.errorOnly(e), registerMap); } } 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 index 79e7e3d910..5b821281c9 100644 --- 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 @@ -67,17 +67,16 @@ class SymPcodeExecutor extends PcodeExecutor { * @return the executor */ public static SymPcodeExecutor forProgram(Program program, SymPcodeExecutorState state, - Reason reason, Set warnings, TaskMonitor monitor) { + Reason reason, 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); + return new SymPcodeExecutor(program, cSpec, language, arithmetic, state, reason, monitor); } private final Program program; private final Register sp; - private final Set warnings; + final Set warnings; private final TaskMonitor monitor; private final DecompInterface decomp = new DecompInterface(); @@ -87,11 +86,11 @@ class SymPcodeExecutor extends PcodeExecutor { public SymPcodeExecutor(Program program, CompilerSpec cSpec, SleighLanguage language, SymPcodeArithmetic arithmetic, SymPcodeExecutorState state, Reason reason, - Set warnings, TaskMonitor monitor) { + TaskMonitor monitor) { super(language, arithmetic, state, reason); this.program = program; this.sp = cSpec.getStackPointer(); - this.warnings = warnings; + this.warnings = state.warnings; this.monitor = monitor; } @@ -217,7 +216,7 @@ class SymPcodeExecutor extends PcodeExecutor { protected FunctionSignature getSignatureFromTargetPointerType(PcodeOpAST op) { VarnodeAST target = (VarnodeAST) op.getInput(0); HighVariable high = target.getHigh(); - + if (high == null) { warnings.add(new NoHighVariableFromTargetPointerTypeUnwindWarning(target)); return null; @@ -243,7 +242,13 @@ class SymPcodeExecutor extends PcodeExecutor { */ protected FunctionSignature getSignatureFromContextAtCallSite(PcodeOpAST op) { FunctionDefinitionDataType sig = new FunctionDefinitionDataType("__indirect"); - sig.setReturnType(op.getOutput().getHigh().getDataType()); + Varnode output = op.getOutput(); + if (output == null) { + sig.setReturnType(VoidDataType.dataType); + } + else { + sig.setReturnType(output.getHigh().getDataType()); + } // input 0 is the target, so drop it. int numInputs = op.getNumInputs(); Parameter[] params = new Parameter[numInputs - 1]; @@ -251,19 +256,19 @@ class SymPcodeExecutor extends PcodeExecutor { for (int i = 1; i < numInputs; i++) { Varnode input = op.getInput(i); HighVariable highVar = input.getHigh(); + DataType dataType = highVar.getDataType(); 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(), + params[i - 1] = new ParameterImpl("param_" + i, dataType, /*new VariableStorage(program, input),*/ program); } catch (InvalidInputException e) { throw new AssertionError(e); } - arguments[i - 1] = new ParameterDefinitionImpl("param_" + i, - input.getHigh().getDataType(), "generated"); + arguments[i - 1] = new ParameterDefinitionImpl("param_" + i, dataType, "generated"); } sig.setArguments(arguments); sig.setComment("generated"); 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 index 589085dbb0..e04771f2a9 100644 --- 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 @@ -15,8 +15,7 @@ */ package ghidra.app.plugin.core.debug.stack; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import ghidra.app.plugin.core.debug.stack.Sym.*; import ghidra.app.plugin.core.debug.stack.SymStateSpace.SymEntry; @@ -50,6 +49,8 @@ public class SymPcodeExecutorState implements PcodeExecutorState { private final SymStateSpace registerSpace; private final SymStateSpace uniqueSpace; + final Set warnings = new LinkedHashSet<>(); + /** * Construct a new state for the given program */ 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 index 4756986759..9b29af9be1 100644 --- 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 @@ -142,7 +142,7 @@ public class UnwindAnalysis { private final BlockGraph graph; private final BlockVertex pcBlock; private final DijkstraShortestPathsAlgorithm pathFinder; - private final Set warnings = new HashSet<>(); + private final Set warnings = new LinkedHashSet<>(); /** * Begin analysis for unwinding a frame, knowing only the program counter for that frame @@ -353,7 +353,7 @@ public class UnwindAnalysis { public SymPcodeExecutorState executeToPc(Deque to) throws CancelledException { SymPcodeExecutorState state = new SymPcodeExecutorState(program); SymPcodeExecutor exec = - SymPcodeExecutor.forProgram(program, state, Reason.EXECUTE_READ, warnings, monitor); + SymPcodeExecutor.forProgram(program, state, Reason.EXECUTE_READ, monitor); executePathTo(exec, to); executeBlockTo(exec, pcBlock.block, pc); return state; @@ -375,7 +375,7 @@ public class UnwindAnalysis { public SymPcodeExecutorState executeFromPc(SymPcodeExecutorState state, Deque from) throws CancelledException { SymPcodeExecutor exec = - SymPcodeExecutor.forProgram(program, state, Reason.EXECUTE_READ, warnings, monitor); + SymPcodeExecutor.forProgram(program, state, Reason.EXECUTE_READ, monitor); executeBlockFrom(exec, pcBlock.block, pc); executePathFrom(exec, from); return state; @@ -454,37 +454,76 @@ public class UnwindAnalysis { "Could not find a path from " + function + " entry to " + pc); } Collection> exitsPaths = getExitsPaths(); + if (exitsPaths.isEmpty()) { + warnings.add(new NoReturnPathStackUnwindWarning(pc)); + } + SymPcodeExecutorState lastSuccessfulEntryState = null; + Exception lastError = null; // TODO: Proper exceptions for useless results for (Deque entryPath : entryPaths) { - SymPcodeExecutorState entryState = executeToPc(entryPath); - Long depth = entryState.computeStackDepth(); - if (depth == null) { + SymPcodeExecutorState entryState; + try { + entryState = executeToPc(entryPath); + } + catch (Exception e) { + lastError = e; continue; } - if (exitsPaths.isEmpty()) { - warnings.add(new NoReturnPathStackUnwindWarning(pc)); + Long depth = entryState.computeStackDepth(); + if (depth == null) { + lastError = new UnwindException("Cannot determine stack depth"); + continue; } + lastSuccessfulEntryState = entryState; + 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) { + SymPcodeExecutorState exitState; + try { + exitState = executeFromPc(entryState.forkRegs(), exitPath); + } + catch (Exception e) { + lastError = e; continue; } + Address addressOfReturn = exitState.computeAddressOfReturn(); + if (addressOfReturn == null) { + lastError = + new UnwindException("Cannot determine address of return pointer"); + continue; + } + Long adjust = exitState.computeStackDepth(); + if (adjust == null) { + lastError = new UnwindException("Cannot determine stack adjustment"); + continue; + } + warnings.addAll(entryState.warnings); + warnings.addAll(exitState.warnings); Map mapByExit = exitState.computeMapUsingRegisters(); mapByExit.entrySet().retainAll(mapByEntry.entrySet()); return new UnwindInfo(function, depth, adjust, addressOfReturn, mapByExit, - new StackUnwindWarningSet(warnings)); + new StackUnwindWarningSet(warnings), null); } - 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); + if (lastSuccessfulEntryState != null) { + warnings.add(new OpaqueReturnPathStackUnwindWarning(pc, lastError)); + try { + long adjust = SymPcodeExecutor.computeStackChange(function, warnings); + return new UnwindInfo(function, lastSuccessfulEntryState.computeStackDepth(), + adjust, null, lastSuccessfulEntryState.computeMapUsingStack(), + new StackUnwindWarningSet(warnings), lastError); + } + catch (Exception e) { + return new UnwindInfo(function, lastSuccessfulEntryState.computeStackDepth(), + null, null, lastSuccessfulEntryState.computeMapUsingStack(), + new StackUnwindWarningSet(warnings), e); + } + } + return new UnwindInfo(function, null, null, null, null, + new StackUnwindWarningSet(warnings), new UnwindException( + "Could not analyze any path from %s entry to %s.\n%s".formatted(function, pc, + lastError.getMessage()), + lastError)); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindException.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindException.java index 8f77ad232c..38b3720b75 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindException.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/UnwindException.java @@ -23,7 +23,7 @@ public class UnwindException extends RuntimeException { super(message); } - public UnwindException(String message, UnwindException cause) { + public UnwindException(String message, Exception 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 index 64f59ddca5..fa3a9172df 100644 --- 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 @@ -32,8 +32,12 @@ 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) { +public record UnwindInfo(Function function, Long depth, Long adjust, Address ofReturn, + Map saved, StackUnwindWarningSet warnings, Exception error) { + + public static UnwindInfo errorOnly(Exception error) { + return new UnwindInfo(null, null, null, null, null, new StackUnwindWarningSet(), error); + } /** * The function that was analyzed @@ -55,7 +59,7 @@ public record UnwindInfo(Function function, long depth, long adjust, Address ofR * * @return the depth */ - public long depth() { + public Long depth() { return depth; } @@ -67,7 +71,7 @@ public record UnwindInfo(Function function, long depth, long adjust, Address ofR * * @return the adjustment */ - public long adjust() { + public Long adjust() { return adjust; } @@ -139,7 +143,7 @@ public record UnwindInfo(Function function, long depth, long adjust, Address ofR * @return the base address */ public Address computeBase(Address spVal) { - return spVal.subtract(depth); + return depth == null ? null : spVal.subtract(depth); } /** 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 index 6628aa3980..12df2e6ae9 100644 --- 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 @@ -124,12 +124,23 @@ public interface UnwoundFrame { /** * Get the warnings generated during analysis * - *

    - * Several warnings may be returned, each on its own line. - * * @return the warnings */ - String getWarnings(); + StackUnwindWarningSet getWarnings(); + + /** + * If the unwind is in error 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 + */ + Exception getError(); /** * Get the value of the storage from the frame 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 index 338a8e7257..979d64155e 100644 --- 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 @@ -602,14 +602,14 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerGUITest { UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor); assertEquals( - new UnwindInfo(function, 0, 4, stack(0), Map.of(), new StackUnwindWarningSet()), + new UnwindInfo(function, 0L, 4L, stack(0), Map.of(), new StackUnwindWarningSet(), null), infoAtEntry); UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor); - assertEquals(new UnwindInfo(function, -20, 4, stack(0), + assertEquals(new UnwindInfo(function, -20L, 4L, stack(0), Map.of( register("EBP"), stack(-4)), - new StackUnwindWarningSet()), + new StackUnwindWarningSet(), null), infoAtBody); } @@ -629,16 +629,17 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerGUITest { UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor); assertEquals( - new UnwindInfo(function, 0, 4, stack(0), Map.of(), new StackUnwindWarningSet()), + new UnwindInfo(function, 0L, 4L, stack(0), Map.of(), new StackUnwindWarningSet(), null), infoAtEntry); UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor); - assertEquals(new UnwindInfo(function, -20, 4, stack(0), + assertEquals(new UnwindInfo(function, -20L, 4L, stack(0), Map.of( register("EBP"), stack(-4)), new StackUnwindWarningSet( new UnspecifiedConventionStackUnwindWarning(myExtern), - new UnknownPurgeStackUnwindWarning(myExtern))), + new UnknownPurgeStackUnwindWarning(myExtern)), + null), infoAtBody); } @@ -656,16 +657,17 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerGUITest { UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor); assertEquals( - new UnwindInfo(function, 0, 4, stack(0), Map.of(), new StackUnwindWarningSet()), + new UnwindInfo(function, 0L, 4L, stack(0), Map.of(), new StackUnwindWarningSet(), null), infoAtEntry); UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor); DataType ptr2Undef = new PointerDataType(DataType.DEFAULT, program.getDataTypeManager()); - assertEquals(new UnwindInfo(function, -20, 4, stack(0), + assertEquals(new UnwindInfo(function, -20L, 4L, stack(0), Map.of( register("EBP"), stack(-4)), new StackUnwindWarningSet( - new UnexpectedTargetTypeStackUnwindWarning(ptr2Undef))), + new UnexpectedTargetTypeStackUnwindWarning(ptr2Undef)), + null), infoAtBody); } @@ -1163,7 +1165,7 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerGUITest { ProgramLocation programLocation, DebuggerCoordinates current, FieldLocation fieldLocation, Field field) throws Throwable { VariableValueTable table = new VariableValueTable(); - List warnings = new ArrayList<>(); + StackUnwindWarningSet warnings = new StackUnwindWarningSet(); waitOn(valuesService.fillVariableValueTable(table, programLocation, current, fieldLocation, field, warnings)); table.add(new WarningsRow(warnings)); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/macho/dyld/DyldChainedStartsOffsets.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/macho/dyld/DyldChainedStartsOffsets.java new file mode 100644 index 0000000000..b8abf9f61f --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/macho/dyld/DyldChainedStartsOffsets.java @@ -0,0 +1,87 @@ +/* ### + * 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.util.bin.format.macho.dyld; + +import java.io.IOException; + +import ghidra.app.util.bin.BinaryReader; +import ghidra.app.util.bin.StructConverter; +import ghidra.app.util.bin.format.macho.MachConstants; +import ghidra.app.util.bin.format.macho.dyld.DyldChainedPtr.DyldChainType; +import ghidra.program.model.data.*; +import ghidra.util.exception.DuplicateNameException; + +/** + * Represents a dyld_chained_starts_offsets structure. + * + * @see mach-o/fixup-chains.h + */ +public class DyldChainedStartsOffsets implements StructConverter { + + private int pointerFormat; + private int startsCount; + private int[] chainStarts; + + /** + * Creates a new {@link DyldChainedStartsOffsets} + * + * @param reader A {@link BinaryReader} positioned at the start of the structure + * @throws IOException if there was an IO-related problem creating the structure + */ + public DyldChainedStartsOffsets(BinaryReader reader) throws IOException { + pointerFormat = reader.readNextInt(); + startsCount = reader.readNextInt(); + chainStarts = reader.readNextIntArray(startsCount); + } + + /** + * Gets the pointer format + * + * @return The pointer format + */ + public DyldChainType getPointerFormat() { + return DyldChainType.lookupChainPtr(pointerFormat); + } + + /** + * Gets the starts count + * + * @return The starts count + */ + public int getStartsCount() { + return startsCount; + } + + /** + * Gets the chain start offsets + * + * @return The chain start offsets + */ + public int[] getChainStartOffsets() { + return chainStarts; + } + + @Override + public DataType toDataType() throws DuplicateNameException, IOException { + StructureDataType struct = new StructureDataType("dyld_chained_starts_offset", 0); + struct.add(DWORD, "pointer_format", "DYLD_CHAINED_PTR_*"); + struct.add(DWORD, "starts_count", "number of starts in array"); + struct.add(new ArrayDataType(IBO32, startsCount, 1), "chain_starts", + "array chain start offsets"); + struct.setCategoryPath(new CategoryPath(MachConstants.DATA_TYPE_CATEGORY)); + return struct; + } +}