diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java index c9adf6cd3e..921e8d9f30 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java @@ -85,9 +85,6 @@ public class DebuggerTrackLocationTrait { if (!value.getEntryKey().equals(TraceStackFrame.KEY_PC)) { return; } - if (!value.getEntryKey().equals(TraceStackFrame.KEY_SP)) { - return; - } TraceStackFrame frame = value.getParent().queryInterface(TraceStackFrame.class); if (frame == null) { return; @@ -107,9 +104,6 @@ public class DebuggerTrackLocationTrait { if (!value.getEntryKey().equals(TraceStackFrame.KEY_PC)) { return; } - if (!value.getEntryKey().equals(TraceStackFrame.KEY_SP)) { - return; - } TraceStackFrame frame = value.getParent().queryInterface(TraceStackFrame.class); if (frame == null) { return; 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 d9215fcb91..b641dd1a8d 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 @@ -819,12 +819,23 @@ public enum VariableValueUtils { public UnwoundFrame getStackFrame(Function function, StackUnwindWarningSet warnings, TaskMonitor monitor, boolean required) { synchronized (lock) { + // We unwind first from the current frame, because we want the closest function + // match above the current frame. + // NB: Unwinding from 0 may provide better info AnalysisUnwoundFrame currentFrame = unwinder.findMatchForFunction(function, coordinates, warnings, monitor); if (currentFrame != null) { return currentFrame; } + // Unwind from 0 for functions below the current frame + currentFrame = unwinder.findMatchForFunction(function, coordinates.frame(0), + warnings, monitor); + if (currentFrame != null) { + warnings.add(new CustomStackUnwindWarning("Unwinding from frame 0")); + return currentFrame; + } + warnings.add( new CustomStackUnwindWarning("Failed to find match for %s among the %d frames." .formatted(function, unwinder.getRecoveredFrameCount()))); 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 3a97275b52..cd803448bf 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 @@ -195,26 +195,19 @@ public class StackUnwinder { continue; } - Address pcVal = pcOrSp(frame, coord, state, true); + Address pcVal = pcOrSp(frame, coord, warnings, state, true); + if (pcVal == null) { + // True if not frame 0 and no access to regs or stack + break; + } ProgramLocation loc = getProgramLocation(coord.getSnap(), pcVal); if (loc != null && service != null) { - UnwindInfo info = - service.getUnwindInfo(loc.getProgram(), loc.getAddress(), monitor); - if (info == null) { - // Continue here to generate a frame and prevent recalculating info - try { - StaticAndUnwind sau = computeUnwindInfo(coord.getSnap(), pcVal, monitor); - info = sau.info(); - } - catch (CancelledException e) { - warnings.add( - new CustomStackUnwindWarning("Unwind cancelled for frame " + level)); - } - } + // Created UnwindInfo if it doesn't exist + service.getUnwindInfo(loc.getProgram(), loc.getAddress(), monitor); } - Address spVal = pcOrSp(frame, coord, state, false); + Address spVal = pcOrSp(frame, coord, warnings, state, false); SavedRegisterMap nextRegisterMap = updateMap(frame, registerMap); frame = unwind(coord, pcVal, spVal, state, nextRegisterMap, monitor); @@ -243,7 +236,7 @@ public class StackUnwinder { } private Address pcOrSp(AnalysisUnwoundFrame frame, - DebuggerCoordinates coordinates, + DebuggerCoordinates coordinates, StackUnwindWarningSet warnings, PcodeExecutorState state, boolean getPc) { TraceThread thread = coordinates.getThread(); int level = coordinates.getFrame(); @@ -284,20 +277,30 @@ public class StackUnwinder { UnwindInfo prevInfo = frame.getUnwindInfo(); Address base = frame.getBasePointer(); try { - regVal = getPc ? prevInfo.computeNextPc(base, state, codeSpace, pc) - : prevInfo.computeNextSp(base); - if (regVal != null) { - return regVal; + if (prevInfo.ofReturn() == null) { + warnings.add(new CustomStackUnwindWarning( + "Indeterminate return from frame preceding " + level)); + } + else { + regVal = getPc ? prevInfo.computeNextPc(base, state, codeSpace, pc) + : prevInfo.computeNextSp(base); + if (regVal != null) { + return regVal; + } } } catch (Exception e) { - // Use the fall-back case below + warnings.add(new CustomStackUnwindWarning(e.getMessage())); } } // Fall-back to current frame - RegisterValue rval = state.inspectRegisterValue(getPc ? pc : sp); - return codeSpace.getAddress(rval.getUnsignedValue().longValue()); + if (coordinates.getFrame() == 0) { + RegisterValue rval = state.inspectRegisterValue(getPc ? pc : sp); + return codeSpace.getAddress(rval.getUnsignedValue().longValue()); + } + + return null; } record StaticAndUnwind(Address staticPc, UnwindInfo info) {} diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/dbgeng/rmi/DbgEngStackUnwindTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/dbgeng/rmi/DbgEngStackUnwindTest.java new file mode 100644 index 0000000000..f0daffb95f --- /dev/null +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/dbgeng/rmi/DbgEngStackUnwindTest.java @@ -0,0 +1,648 @@ +/* ### + * 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 agent.dbgeng.rmi; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.net.*; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import db.Transaction; +import docking.widgets.fieldpanel.Layout; +import docking.widgets.fieldpanel.field.Field; +import docking.widgets.fieldpanel.listener.IndexMapper; +import docking.widgets.fieldpanel.listener.LayoutModelListener; +import docking.widgets.fieldpanel.support.FieldLocation; +import generic.test.category.NightlyCategory; +import ghidra.app.decompiler.*; +import ghidra.app.decompiler.component.*; +import ghidra.app.decompiler.location.DefaultDecompilerLocation; +import ghidra.app.plugin.assembler.AssemblySelector; +import ghidra.app.plugin.assembler.AssemblySemanticException; +import ghidra.app.plugin.assembler.sleigh.sem.*; +import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; +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.modules.DebuggerStaticMappingServicePlugin; +import ghidra.app.plugin.core.debug.stack.StackUnwindWarningSet; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +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.DebuggerStaticMappingService; +import ghidra.app.util.viewer.field.FieldFactory; +import ghidra.app.util.viewer.field.ListingField; +import ghidra.app.util.viewer.listingpanel.ListingPanel; +import ghidra.debug.api.tracemgr.DebuggerCoordinates; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainFolder; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.*; +import ghidra.program.util.*; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.database.module.DBTraceStaticMappingManager; +import ghidra.trace.model.Trace; +import ghidra.trace.model.modules.TraceStaticMapping; +import ghidra.util.InvalidNameException; +import ghidra.util.NumericUtilities; +import ghidra.util.exception.CancelledException; +import junit.framework.AssertionFailedError; + +@Category(NightlyCategory.class) // this may actually be an @PortSensitive test +public class DbgEngStackUnwindTest extends AbstractDbgEngTraceRmiTest { + + Address stackRefInstr; + protected List programs; + + CodeBrowserPlugin codeBrowserPlugin; + ListingPanel staticListing; + DebuggerListingPlugin listingPlugin; + VariableValueHoverPlugin valuesPlugin; + ListingPanel dynamicListing; + VariableValueHoverService valuesService; + DebuggerStaticMappingService mappingService; + DecompilerProvider decompilerProvider; + DecompilerPanel decompilerPanel; + + record PythonAndTrace(PythonAndConnection conn, ManagedDomainObject mdo) + implements AutoCloseable { + public void execute(String cmd) { + conn.execute(cmd); + } + + public String executeCapture(String cmd) { + return conn.executeCapture(cmd); + } + + @Override + public void close() throws Exception { + Exception toThrow = null; + try { + conn.close(); + } + catch (Exception e) { + toThrow = e; + } + try { + mdo.close(); + } + catch (Exception e) { + toThrow = e; + } + if (toThrow != null) { + throw toThrow; + } + } + } + + @SuppressWarnings("resource") + protected PythonAndTrace startAndSyncPython(ManagedDomainObject mdo) throws Exception { + PythonAndConnection conn = startAndConnectPython(); + try { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + return new PythonAndTrace(conn, mdo); + } + catch (Exception e) { + conn.close(); + throw e; + } + } + + protected long lastSnap(PythonAndTrace conn) { + return conn.conn.connection().getLastSnapshot(tb.trace); + } + + @Test + public void testBasicUnwind() throws Throwable { + List files = Arrays.asList("kernel32.dll", "expSpin.exe", "ntdll.dll"); + + addPlugins(); + + DomainFile[] dfs = getDomainFiles(files); + tool.acceptDomainFiles(dfs); + + DomainFile traceDF = getDomainFile("expSpin_dbgeng"); + ManagedDomainObject mdo = new ManagedDomainObject(traceDF, false, false, monitor); + resyncMappings((Trace) mdo.get()); + tool.acceptDomainFiles(new DomainFile[] { traceDF }); + + programs = openPrograms(dfs); + + try (PythonAndTrace conn = startAndSyncPython(mdo)) { + traceManager.activateSnap(2); + + waitForPass(() -> assertEquals(Boolean.TRUE, + traceManager.getCurrent().getSnap() == 2)); + + checkRtlUserThreadStartUnwindFrom0(2); + checkMainUnwindFrom0(1); + checkSleepUnwindFrom0(0); + } + } + + @Test + public void testUnwindActivateMidStack() throws Throwable { + List files = Arrays.asList("kernel32.dll", "expSpin.exe", "ntdll.dll"); + + addPlugins(); + + DomainFile[] dfs = getDomainFiles(files); + tool.acceptDomainFiles(dfs); + + DomainFile traceDF = getDomainFile("expSpin_dbgeng"); + ManagedDomainObject mdo = new ManagedDomainObject(traceDF, false, false, monitor); + resyncMappings((Trace) mdo.get()); + tool.acceptDomainFiles(new DomainFile[] { traceDF }); + + programs = openPrograms(dfs); + + try (PythonAndTrace conn = startAndSyncPython(mdo)) { + traceManager.activateSnap(2); + + waitForPass(() -> assertEquals(Boolean.TRUE, + traceManager.getCurrent().getSnap() == 2)); + + traceManager.activateFrame(1); + + checkRtlUserThreadStartUnwindAlt(2); + checkMainUnwindFrom0(1); + checkSleepUnwindFrom0(0); + } + } + + @Test + public void testUnwindActivateHighFrame() throws Throwable { + List files = Arrays.asList("kernel32.dll", "expSpin.exe", "ntdll.dll"); + + addPlugins(); + + DomainFile[] dfs = getDomainFiles(files); + tool.acceptDomainFiles(dfs); + + DomainFile traceDF = getDomainFile("expSpin_dbgeng"); + ManagedDomainObject mdo = new ManagedDomainObject(traceDF, false, false, monitor); + resyncMappings((Trace) mdo.get()); + tool.acceptDomainFiles(new DomainFile[] { traceDF }); + + programs = openPrograms(dfs); + + try (PythonAndTrace conn = startAndSyncPython(mdo)) { + traceManager.activateSnap(2); + + waitForPass(() -> assertEquals(Boolean.TRUE, + traceManager.getCurrent().getSnap() == 2)); + + traceManager.activateFrame(4); + + checkRtlUserThreadStartUnwindAlt(2); + checkMainUnwindFrom0(1); + checkSleepUnwindFrom0(0); + } + } + + @Test + public void testBasicUnwindMissingLowFrame() throws Throwable { + List files = Arrays.asList("expSpin.exe", "ntdll.dll"); + + addPlugins(); + + DomainFile[] dfs = getDomainFiles(files); + tool.acceptDomainFiles(dfs); + + DomainFile traceDF = getDomainFile("expSpin_dbgeng"); + ManagedDomainObject mdo = new ManagedDomainObject(traceDF, false, false, monitor); + resyncMappings((Trace) mdo.get()); + tool.acceptDomainFiles(new DomainFile[] { traceDF }); + + programs = openPrograms(dfs); + + try (PythonAndTrace conn = startAndSyncPython(mdo)) { + traceManager.activateSnap(2); + + waitForPass(() -> assertEquals(Boolean.TRUE, + traceManager.getCurrent().getSnap() == 2)); + + checkRtlUserThreadStartUnwindAlt(1); + checkMainUnwindFrom0(0); + } + } + + @Test + public void testBasicUnwindMissingMedFrame() throws Throwable { + List files = Arrays.asList("kernel32.dll", "ntdll.dll"); + + addPlugins(); + + DomainFile[] dfs = getDomainFiles(files); + tool.acceptDomainFiles(dfs); + + DomainFile traceDF = getDomainFile("expSpin_dbgeng"); + ManagedDomainObject mdo = new ManagedDomainObject(traceDF, false, false, monitor); + resyncMappings((Trace) mdo.get()); + tool.acceptDomainFiles(new DomainFile[] { traceDF }); + + programs = openPrograms(dfs); + + try (PythonAndTrace conn = startAndSyncPython(mdo)) { + traceManager.activateSnap(2); + + waitForPass(() -> assertEquals(Boolean.TRUE, + traceManager.getCurrent().getSnap() == 2)); + + checkRtlUserThreadStartUnwindAlt(1); + checkSleepUnwindFrom0(0); + } + } + + @Test + public void testBasicUnwindMissingHighFrame() throws Throwable { + List files = Arrays.asList("kernel32.dll", "expSpin.exe"); + + addPlugins(); + + DomainFile[] dfs = getDomainFiles(files); + tool.acceptDomainFiles(dfs); + + DomainFile traceDF = getDomainFile("expSpin_dbgeng"); + ManagedDomainObject mdo = new ManagedDomainObject(traceDF, false, false, monitor); + resyncMappings((Trace) mdo.get()); + tool.acceptDomainFiles(new DomainFile[] { traceDF }); + + programs = openPrograms(dfs); + + try (PythonAndTrace conn = startAndSyncPython(mdo)) { + traceManager.activateSnap(2); + + waitForPass(() -> assertEquals(Boolean.TRUE, + traceManager.getCurrent().getSnap() == 2)); + + checkMainUnwindFrom0(1); + checkSleepUnwindFrom0(0); + } + } + + private void checkSleepUnwindFrom0(int at) throws Throwable { + programManager.openProgram(programs.get(at)); + VariableValueTable table = + getTable(programs.get(at), 0x18001b100L, "dwMilliseconds", "Sleep(dwMilliseconds);"); + assertTable(Map.of( + RowKey.NAME, "Name: dwMilliseconds", + RowKey.FRAME, + "Frame: 0 Sleep pc=7ff909fab100 sp=be6c2ff798 base=be6c2ff798", + RowKey.STORAGE, "Storage: ECX:4", + RowKey.TYPE, "Type: DWORD", + RowKey.LOCATION, "Location: ECX:4", + RowKey.INTEGER, "Integer: (UNKNOWN) 1000, 0x3e8", + RowKey.VALUE, "Value: (UNKNOWN) 3E8h", + RowKey.WARNINGS, "IGNORED"), table); + } + + private void checkMainUnwindFrom0(int at) throws Throwable { + programManager.openProgram(programs.get(at)); + VariableValueTable table = + getTable(programs.get(at), 0x140001000L, "local_58", + "for (local_58 = 0; local_58 < 10; local_58 = local_58 + 1) {"); + assertTable(Map.of( + RowKey.NAME, "Name: local_58", + RowKey.FRAME, + "Frame: 1 FUN_140001000 pc=7ff7f73c1040 sp=be6c2ff7a0 base=be6c2ff818", + RowKey.STORAGE, "Storage: Stack[-0x58]:4", + RowKey.TYPE, "Type: undefined4", + RowKey.LOCATION, "Location: be6c2ff7c0:4", + RowKey.BYTES, "Bytes: (UNKNOWN) 00 00 00 00", + RowKey.INTEGER, "Integer: (UNKNOWN) 0", + RowKey.VALUE, "Value: (UNKNOWN) 00000000h", + RowKey.WARNINGS, "IGNORED"), table); + } + + private void checkRtlUserThreadStartUnwindFrom0(int at) throws Throwable { + programManager.openProgram(programs.get(at)); + VariableValueTable table = + getTable(programs.get(at), 0x18007edd0L, "uVar1", "uVar1 = (*param_1)(param_2);"); + assertTable(Map.of( + RowKey.NAME, "Name: uVar1", + RowKey.FRAME, + "Frame: 4 RtlUserThreadStart pc=7ff90adfedfb sp=be6c2ff890 base=be6c2ff908", + RowKey.STORAGE, "Storage: EAX:4", + RowKey.TYPE, "Type: undefined4", + RowKey.LOCATION, "Location: EAX:4", + RowKey.INTEGER, "Integer: (UNKNOWN) 3543813394, 0xd33a4d12\n-751153902, -0x2cc5b2ee", + RowKey.VALUE, "Value: (UNKNOWN) D33A4D12h", + RowKey.WARNINGS, "IGNORED"), table); + } + + private void checkRtlUserThreadStartUnwindAlt(int at) throws Throwable { + programManager.openProgram(programs.get(at)); + VariableValueTable table = + getTable(programs.get(at), 0x18007edd0L, "uVar1", "uVar1 = (*param_1)(param_2);"); + assertTable(Map.of( + RowKey.NAME, "Name: uVar1", + RowKey.FRAME, + "Frame: 4 RtlUserThreadStart pc=7ff90adfedfb sp=be6c2ff890 base=be6c2ff908", + RowKey.STORAGE, "Storage: EAX:4", + RowKey.TYPE, "Type: undefined4", + RowKey.LOCATION, "Location: EAX:4", + RowKey.INTEGER, "Integer: (UNKNOWN) 0", + RowKey.VALUE, "Value: (UNKNOWN) 00000000h", + RowKey.WARNINGS, "IGNORED"), table); + } + + private VariableValueTable getTable(Program p, long addr, String tokText, String fieldText) + throws Throwable { + Address entry = addr(p, addr); + ProgramLocation pl = new ProgramLocation(p, entry); + goTo(staticListing, pl); + Function f = p.getFunctionManager().getFunctionAt(entry); + HoverLocation loc = findTokenLocation(decompilerPanel, f, tokText, fieldText); + return getVariableValueTable(loc.pLoc, traceManager.getCurrent(), loc.fLoc, loc.field); + } + + private DomainFile[] getDomainFiles(List files) + throws InvalidNameException, CancelledException, IOException { + DomainFile[] dfs = new DomainFile[files.size()]; + int i = 0; + for (String fname : files) { + dfs[i++] = getDomainFile(fname); + } + return dfs; + } + + private DomainFile getDomainFile(String fname) + throws InvalidNameException, CancelledException, IOException { + DomainFolder rootFolder = tool.getProject() + .getProjectData() + .getRootFolder(); + File f = getTestDataFile(fname + ".gzf"); + return rootFolder.createFile(fname, f, monitor); + } + + private List openPrograms(DomainFile[] files) { + List progs = new ArrayList<>(); + for (int i = 0; i < files.length; i++) { + progs.add(programManager.openProgram(files[i])); + } + return progs; + } + + private void resyncMappings(Trace trace) + throws MalformedURLException, URISyntaxException { + DomainFolder rootFolder = tool.getProject() + .getProjectData() + .getRootFolder(); + DBTraceStaticMappingManager staticMappingManager = + (DBTraceStaticMappingManager) trace.getStaticMappingManager(); + Collection allEntries = + staticMappingManager.getAllEntries(); + Collection newEntries = new HashSet(); + for (TraceStaticMapping mapping : allEntries) { + newEntries.add(mapping); + } + try (Transaction tx = trace.openTransaction("Remove .text mapping")) { + for (TraceStaticMapping mapping : newEntries) { + mapping.delete(); + } + } + for (TraceStaticMapping mapping : newEntries) { + String previousRoot = mapping.getStaticProgramURL().toString(); + String fileName = previousRoot.substring(previousRoot.lastIndexOf("?/") + 2); + URL localProjectURL = rootFolder.getLocalProjectURL(); + URL url = new URI(localProjectURL + fileName).toURL(); + try (Transaction tx = trace.openTransaction("Remove .text mapping")) { + staticMappingManager.add(mapping.getTraceAddressRange(), mapping.getLifespan(), + url, mapping.getStaticAddress()); + } + } + } + + protected void addPlugins() throws Throwable { + codeBrowserPlugin = addPlugin(tool, CodeBrowserPlugin.class); + staticListing = codeBrowserPlugin.getProvider().getListingPanel(); + listingPlugin = addPlugin(tool, DebuggerListingPlugin.class); + valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + mappingService = addPlugin(tool, DebuggerStaticMappingServicePlugin.class); + dynamicListing = listingPlugin.getProvider().getListingPanel(); + //addPlugin(tool, DebuggerControlPlugin.class); + //addPlugin(tool, DebuggerStaticMappingPlugin.class); + //addPlugin(tool, DebuggerModelPlugin.class); + addPlugin(tool, DisassemblerPlugin.class); + addPlugin(tool, DecompilePlugin.class); + valuesService = valuesPlugin.getHoverService(); + + decompilerProvider = waitForComponentProvider(DecompilerProvider.class); + decompilerPanel = decompilerProvider.getDecompilerPanel(); + tool.showComponentProvider(decompilerProvider, true); + } + + 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)); + }); + } + + public static HoverLocation findTokenLocation(DecompilerPanel decompilerPanel, + Function function, String tokText, String fieldText) { + DecompileResults results = waitForValue(() -> { + ProgramLocation pLoc; + try { + pLoc = decompilerPanel.getCurrentLocation(); + } + catch (NullPointerException e) { + /** + * HACK: There's an unlikely race condition where the layout controller has created + * the array of layouts but not fully populated it by the time we ask for the + * current location. This may cause a line we inspect to still have null in it and + * throw an NPE. Whatever. Just catch the thing and return null so that we try + * again. As far as I can tell, this is not indicative of a problem in production, + * because the controller won't issue an updated event until that array is fully + * populated. + */ + return null; + } + 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().trim())) { + 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())) { + + Address entryPoint = function.getEntryPoint(); + DecompilerLocationInfo info = + new DecompilerLocationInfo(entryPoint, results, token, + i.intValue(), 0); + DefaultDecompilerLocation loc = token.getMinAddress() == null ? null + : new DefaultDecompilerLocation(program, + token.getMinAddress(), info); + return new HoverLocation(loc, fLoc, field, token); + } + } + } + } + } + return null; + }); + } + + protected HoverLocation findTokenLocation(Function function, String tokText, String fieldText) + throws Throwable { + CompletableFuture ready = new CompletableFuture<>(); + decompilerPanel.getLayoutController().addLayoutModelListener(new LayoutModelListener() { + @Override + public void modelSizeChanged(IndexMapper indexMapper) { + if (decompilerPanel.getCurrentLocation() != null) { + ready.complete(null); + } + } + + @Override + public void dataChanged(BigInteger start, BigInteger end) { + } + }); + tool.showComponentProvider(decompilerProvider, true); + ready.get(5, TimeUnit.SECONDS); + try { + return findTokenLocation(decompilerPanel, function, tokText, fieldText); + } + catch (AssertionFailedError e) { + throw e; + } + } + + 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(ProgramLocation programLocation, + DebuggerCoordinates current, FieldLocation fieldLocation, Field field) + throws Throwable { + VariableValueTable table = new VariableValueTable(); + StackUnwindWarningSet warnings = new StackUnwindWarningSet(); + waitOn(valuesService.fillVariableValueTable(table, programLocation, current, fieldLocation, + field, warnings)); + table.add(new WarningsRow(warnings)); + return table; + } + + public static final AssemblySelector NO_16BIT_CALLS = new AssemblySelector() { + @Override + public Selection 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 new Selection(res.getInstruction().fillMask(), res.getContext()); + } + throw new AssemblySemanticException(semanticErrors); + } + }; + +} diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbStackUnwindTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbStackUnwindTest.java new file mode 100644 index 0000000000..43258e27c4 --- /dev/null +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbStackUnwindTest.java @@ -0,0 +1,661 @@ +/* ### + * 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 agent.lldb.rmi; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.net.*; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import db.Transaction; +import docking.widgets.fieldpanel.Layout; +import docking.widgets.fieldpanel.field.Field; +import docking.widgets.fieldpanel.listener.IndexMapper; +import docking.widgets.fieldpanel.listener.LayoutModelListener; +import docking.widgets.fieldpanel.support.FieldLocation; +import generic.test.category.NightlyCategory; +import ghidra.app.decompiler.*; +import ghidra.app.decompiler.component.*; +import ghidra.app.decompiler.location.DefaultDecompilerLocation; +import ghidra.app.plugin.assembler.AssemblySelector; +import ghidra.app.plugin.assembler.AssemblySemanticException; +import ghidra.app.plugin.assembler.sleigh.sem.*; +import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; +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.modules.DebuggerStaticMappingServicePlugin; +import ghidra.app.plugin.core.debug.stack.StackUnwindWarningSet; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +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.DebuggerStaticMappingService; +import ghidra.app.util.viewer.field.FieldFactory; +import ghidra.app.util.viewer.field.ListingField; +import ghidra.app.util.viewer.listingpanel.ListingPanel; +import ghidra.debug.api.tracemgr.DebuggerCoordinates; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainFolder; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.*; +import ghidra.program.util.*; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.database.module.DBTraceStaticMappingManager; +import ghidra.trace.model.Trace; +import ghidra.trace.model.modules.TraceStaticMapping; +import ghidra.util.InvalidNameException; +import ghidra.util.NumericUtilities; +import ghidra.util.exception.CancelledException; +import junit.framework.AssertionFailedError; + +@Category(NightlyCategory.class) // this may actually be an @PortSensitive test +public class LldbStackUnwindTest extends AbstractLldbTraceRmiTest { + + Address stackRefInstr; + protected List programs; + + CodeBrowserPlugin codeBrowserPlugin; + ListingPanel staticListing; + DebuggerListingPlugin listingPlugin; + VariableValueHoverPlugin valuesPlugin; + ListingPanel dynamicListing; + VariableValueHoverService valuesService; + DebuggerStaticMappingService mappingService; + DecompilerProvider decompilerProvider; + DecompilerPanel decompilerPanel; + + record LldbAndTrace(LldbAndConnection conn, ManagedDomainObject mdo) implements AutoCloseable { + public void execute(String cmd) { + conn.execute(cmd); + } + + public String executeCapture(String cmd) { + return conn.executeCapture(cmd); + } + + public void success() { + conn.success(); + } + + @Override + public void close() throws Exception { + Exception toThrow = null; + try { + conn.close(); + } + catch (Exception e) { + toThrow = e; + } + try { + mdo.close(); + } + catch (Exception e) { + toThrow = e; + } + if (toThrow != null) { + throw toThrow; + } + } + } + + @SuppressWarnings("resource") + protected LldbAndTrace startAndSyncLldb(ManagedDomainObject mdo) throws Exception { + LldbAndConnection conn = startAndConnectLldb(); + try { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + return new LldbAndTrace(conn, mdo); + } + catch (Exception e) { + conn.close(); + throw e; + } + } + + protected long lastSnap(LldbAndTrace conn) { + return conn.conn.connection().getLastSnapshot(tb.trace); + } + + @Test + public void testBasicUnwind() throws Throwable { + List files = Arrays.asList("kernel32.dll", "expSpin.exe", "ntdll.dll"); + + addPlugins(); + + DomainFile[] dfs = getDomainFiles(files); + tool.acceptDomainFiles(dfs); + + DomainFile traceDF = getDomainFile("expSpin_lldb"); + ManagedDomainObject mdo = new ManagedDomainObject(traceDF, false, false, monitor); + resyncMappings((Trace) mdo.get()); + tool.acceptDomainFiles(new DomainFile[] { traceDF }); + + programs = openPrograms(dfs); + + try (LldbAndTrace conn = startAndSyncLldb(mdo)) { + traceManager.activateSnap(2); + + waitForPass(() -> assertEquals(Boolean.TRUE, + traceManager.getCurrent().getSnap() == 2)); + + checkRtlUserThreadStartUnwindFrom0(2); + checkMainUnwindFrom0(1); + checkSleepUnwindFrom0(0); + + conn.success(); + } + } + + @Test + public void testUnwindActivateMidStack() throws Throwable { + List files = Arrays.asList("kernel32.dll", "expSpin.exe", "ntdll.dll"); + + addPlugins(); + + DomainFile[] dfs = getDomainFiles(files); + tool.acceptDomainFiles(dfs); + + DomainFile traceDF = getDomainFile("expSpin_lldb"); + ManagedDomainObject mdo = new ManagedDomainObject(traceDF, false, false, monitor); + resyncMappings((Trace) mdo.get()); + tool.acceptDomainFiles(new DomainFile[] { traceDF }); + + programs = openPrograms(dfs); + + try (LldbAndTrace conn = startAndSyncLldb(mdo)) { + traceManager.activateSnap(2); + + waitForPass(() -> assertEquals(Boolean.TRUE, + traceManager.getCurrent().getSnap() == 2)); + + traceManager.activateFrame(1); + + checkRtlUserThreadStartUnwindAlt(2); + checkMainUnwindFrom0(1); + checkSleepUnwindFrom0(0); + + conn.success(); + } + } + + @Test + public void testUnwindActivateHighFrame() throws Throwable { + List files = Arrays.asList("kernel32.dll", "expSpin.exe", "ntdll.dll"); + + addPlugins(); + + DomainFile[] dfs = getDomainFiles(files); + tool.acceptDomainFiles(dfs); + + DomainFile traceDF = getDomainFile("expSpin_lldb"); + ManagedDomainObject mdo = new ManagedDomainObject(traceDF, false, false, monitor); + resyncMappings((Trace) mdo.get()); + tool.acceptDomainFiles(new DomainFile[] { traceDF }); + + programs = openPrograms(dfs); + + try (LldbAndTrace conn = startAndSyncLldb(mdo)) { + traceManager.activateSnap(2); + + waitForPass(() -> assertEquals(Boolean.TRUE, + traceManager.getCurrent().getSnap() == 2)); + + traceManager.activateFrame(4); + + checkRtlUserThreadStartUnwindAlt(2); + checkMainUnwindFrom0(1); + checkSleepUnwindFrom0(0); + + conn.success(); + } + } + + @Test + public void testBasicUnwindMissingLowFrame() throws Throwable { + List files = Arrays.asList("expSpin.exe", "ntdll.dll"); + + addPlugins(); + + DomainFile[] dfs = getDomainFiles(files); + tool.acceptDomainFiles(dfs); + + DomainFile traceDF = getDomainFile("expSpin_lldb"); + ManagedDomainObject mdo = new ManagedDomainObject(traceDF, false, false, monitor); + resyncMappings((Trace) mdo.get()); + tool.acceptDomainFiles(new DomainFile[] { traceDF }); + + programs = openPrograms(dfs); + + try (LldbAndTrace conn = startAndSyncLldb(mdo)) { + traceManager.activateSnap(2); + + waitForPass(() -> assertEquals(Boolean.TRUE, + traceManager.getCurrent().getSnap() == 2)); + + checkRtlUserThreadStartUnwindAlt(1); + checkMainUnwindFrom0(0); + + conn.success(); + } + } + + @Test + public void testBasicUnwindMissingMedFrame() throws Throwable { + List files = Arrays.asList("kernel32.dll", "ntdll.dll"); + + addPlugins(); + + DomainFile[] dfs = getDomainFiles(files); + tool.acceptDomainFiles(dfs); + + DomainFile traceDF = getDomainFile("expSpin_lldb"); + ManagedDomainObject mdo = new ManagedDomainObject(traceDF, false, false, monitor); + resyncMappings((Trace) mdo.get()); + tool.acceptDomainFiles(new DomainFile[] { traceDF }); + + programs = openPrograms(dfs); + + try (LldbAndTrace conn = startAndSyncLldb(mdo)) { + traceManager.activateSnap(2); + + waitForPass(() -> assertEquals(Boolean.TRUE, + traceManager.getCurrent().getSnap() == 2)); + + checkRtlUserThreadStartUnwindAlt(1); + checkSleepUnwindFrom0(0); + + conn.success(); + } + } + + @Test + public void testBasicUnwindMissingHighFrame() throws Throwable { + List files = Arrays.asList("kernel32.dll", "expSpin.exe"); + + addPlugins(); + + DomainFile[] dfs = getDomainFiles(files); + tool.acceptDomainFiles(dfs); + + DomainFile traceDF = getDomainFile("expSpin_lldb"); + ManagedDomainObject mdo = new ManagedDomainObject(traceDF, false, false, monitor); + resyncMappings((Trace) mdo.get()); + tool.acceptDomainFiles(new DomainFile[] { traceDF }); + + programs = openPrograms(dfs); + + try (LldbAndTrace conn = startAndSyncLldb(mdo)) { + traceManager.activateSnap(2); + + waitForPass(() -> assertEquals(Boolean.TRUE, + traceManager.getCurrent().getSnap() == 2)); + + checkMainUnwindFrom0(1); + checkSleepUnwindFrom0(0); + + conn.success(); + } + } + + private void checkSleepUnwindFrom0(int at) throws Throwable { + programManager.openProgram(programs.get(at)); + VariableValueTable table = + getTable(programs.get(at), 0x18001b100L, "dwMilliseconds", "Sleep(dwMilliseconds);"); + assertTable(Map.of( + RowKey.NAME, "Name: dwMilliseconds", + RowKey.FRAME, + "Frame: 0 Sleep pc=7fffb7c1b100 sp=f9a118fd78 base=f9a118fd78", + RowKey.STORAGE, "Storage: ECX:4", + RowKey.TYPE, "Type: DWORD", + RowKey.LOCATION, "Location: ECX:4", + RowKey.INTEGER, "Integer: (KNOWN) 1000, 0x3e8", + RowKey.VALUE, "Value: (KNOWN) 3E8h", + RowKey.WARNINGS, "IGNORED"), table); + } + + private void checkMainUnwindFrom0(int at) throws Throwable { + programManager.openProgram(programs.get(at)); + VariableValueTable table = + getTable(programs.get(at), 0x140001000L, "local_58", + "for (local_58 = 0; local_58 < 10; local_58 = local_58 + 1) {"); + assertTable(Map.of( + RowKey.NAME, "Name: local_58", + RowKey.FRAME, + "Frame: 1 FUN_140001000 pc=7ff6bd611040 sp=f900000000 base=f900000078", + RowKey.STORAGE, "Storage: Stack[-0x58]:4", + RowKey.TYPE, "Type: undefined4", + RowKey.LOCATION, "Location: f900000020:4", + RowKey.BYTES, "Bytes: (UNKNOWN) 00 00 00 00", + RowKey.INTEGER, "Integer: (UNKNOWN) 0", + RowKey.VALUE, "Value: (UNKNOWN) 00000000h", + RowKey.WARNINGS, "IGNORED"), table); + } + + private void checkRtlUserThreadStartUnwindFrom0(int at) throws Throwable { + programManager.openProgram(programs.get(at)); + VariableValueTable table = + getTable(programs.get(at), 0x18007edd0L, "uVar1", "uVar1 = (*param_1)(param_2);"); + assertTable(Map.of( + RowKey.NAME, "Name: uVar1", + RowKey.FRAME, + "Frame: 4 RtlUserThreadStart pc=7fffb7e3edfb sp=f900000000 base=f900000078", + RowKey.STORAGE, "Storage: EAX:4", + RowKey.TYPE, "Type: undefined4", + RowKey.LOCATION, "Location: EAX:4", + RowKey.INTEGER, "Integer: (KNOWN) 1193036728, 0x471c4bb8", + RowKey.VALUE, "Value: (KNOWN) 471C4BB8h", + RowKey.WARNINGS, "IGNORED"), table); + } + + private void checkRtlUserThreadStartUnwindAlt(int at) throws Throwable { + programManager.openProgram(programs.get(at)); + VariableValueTable table = + getTable(programs.get(at), 0x18007edd0L, "uVar1", "uVar1 = (*param_1)(param_2);"); + assertTable(Map.of( + RowKey.NAME, "Name: uVar1", + RowKey.FRAME, + "Frame: 4 RtlUserThreadStart pc=7fffb7e3edfb sp=f900000000 base=f900000078", + RowKey.STORAGE, "Storage: EAX:4", + RowKey.TYPE, "Type: undefined4", + RowKey.LOCATION, "Location: EAX:4", + RowKey.INTEGER, "Integer: (KNOWN) 2702704640, 0xa1180000\n-1592262656, -0x5ee80000", + RowKey.VALUE, "Value: (KNOWN) A1180000h", + RowKey.WARNINGS, "IGNORED"), table); + } + + private VariableValueTable getTable(Program p, long addr, String tokText, String fieldText) + throws Throwable { + Address entry = addr(p, addr); + ProgramLocation pl = new ProgramLocation(p, entry); + goTo(staticListing, pl); + Function f = p.getFunctionManager().getFunctionAt(entry); + HoverLocation loc = findTokenLocation(decompilerPanel, f, tokText, fieldText); + return getVariableValueTable(loc.pLoc, traceManager.getCurrent(), loc.fLoc, loc.field); + } + + private DomainFile[] getDomainFiles(List files) + throws InvalidNameException, CancelledException, IOException { + DomainFile[] dfs = new DomainFile[files.size()]; + int i = 0; + for (String fname : files) { + dfs[i++] = getDomainFile(fname); + } + return dfs; + } + + private DomainFile getDomainFile(String fname) + throws InvalidNameException, CancelledException, IOException { + DomainFolder rootFolder = tool.getProject() + .getProjectData() + .getRootFolder(); + File f = getTestDataFile(fname + ".gzf"); + return rootFolder.createFile(fname, f, monitor); + } + + private List openPrograms(DomainFile[] files) { + List progs = new ArrayList<>(); + for (int i = 0; i < files.length; i++) { + progs.add(programManager.openProgram(files[i])); + } + return progs; + } + + private void resyncMappings(Trace trace) + throws MalformedURLException, URISyntaxException { + DomainFolder rootFolder = tool.getProject() + .getProjectData() + .getRootFolder(); + DBTraceStaticMappingManager staticMappingManager = + (DBTraceStaticMappingManager) trace.getStaticMappingManager(); + Collection allEntries = + staticMappingManager.getAllEntries(); + Collection newEntries = new HashSet(); + for (TraceStaticMapping mapping : allEntries) { + newEntries.add(mapping); + } + try (Transaction tx = trace.openTransaction("Remove .text mapping")) { + for (TraceStaticMapping mapping : newEntries) { + mapping.delete(); + } + for (TraceStaticMapping mapping : newEntries) { + String previousRoot = mapping.getStaticProgramURL().toString(); + String fileName = previousRoot.substring(previousRoot.lastIndexOf("?/") + 2); + URL localProjectURL = rootFolder.getLocalProjectURL(); + URL url = new URI(localProjectURL + fileName).toURL(); + staticMappingManager.add(mapping.getTraceAddressRange(), mapping.getLifespan(), + url, mapping.getStaticAddress()); + } + } + } + + protected void addPlugins() throws Throwable { + codeBrowserPlugin = addPlugin(tool, CodeBrowserPlugin.class); + staticListing = codeBrowserPlugin.getProvider().getListingPanel(); + listingPlugin = addPlugin(tool, DebuggerListingPlugin.class); + valuesPlugin = addPlugin(tool, VariableValueHoverPlugin.class); + mappingService = addPlugin(tool, DebuggerStaticMappingServicePlugin.class); + dynamicListing = listingPlugin.getProvider().getListingPanel(); + //addPlugin(tool, DebuggerControlPlugin.class); + //addPlugin(tool, DebuggerStaticMappingPlugin.class); + //addPlugin(tool, DebuggerModelPlugin.class); + addPlugin(tool, DisassemblerPlugin.class); + addPlugin(tool, DecompilePlugin.class); + valuesService = valuesPlugin.getHoverService(); + + decompilerProvider = waitForComponentProvider(DecompilerProvider.class); + decompilerPanel = decompilerProvider.getDecompilerPanel(); + tool.showComponentProvider(decompilerProvider, true); + } + + 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)); + }); + } + + public static HoverLocation findTokenLocation(DecompilerPanel decompilerPanel, + Function function, String tokText, String fieldText) { + DecompileResults results = waitForValue(() -> { + ProgramLocation pLoc; + try { + pLoc = decompilerPanel.getCurrentLocation(); + } + catch (NullPointerException e) { + /** + * HACK: There's an unlikely race condition where the layout controller has created + * the array of layouts but not fully populated it by the time we ask for the + * current location. This may cause a line we inspect to still have null in it and + * throw an NPE. Whatever. Just catch the thing and return null so that we try + * again. As far as I can tell, this is not indicative of a problem in production, + * because the controller won't issue an updated event until that array is fully + * populated. + */ + return null; + } + 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().trim())) { + 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())) { + + Address entryPoint = function.getEntryPoint(); + DecompilerLocationInfo info = + new DecompilerLocationInfo(entryPoint, results, token, + i.intValue(), 0); + DefaultDecompilerLocation loc = token.getMinAddress() == null ? null + : new DefaultDecompilerLocation(program, + token.getMinAddress(), info); + return new HoverLocation(loc, fLoc, field, token); + } + } + } + } + } + return null; + }); + } + + protected HoverLocation findTokenLocation(Function function, String tokText, String fieldText) + throws Throwable { + CompletableFuture ready = new CompletableFuture<>(); + decompilerPanel.getLayoutController().addLayoutModelListener(new LayoutModelListener() { + @Override + public void modelSizeChanged(IndexMapper indexMapper) { + if (decompilerPanel.getCurrentLocation() != null) { + ready.complete(null); + } + } + + @Override + public void dataChanged(BigInteger start, BigInteger end) { + } + }); + tool.showComponentProvider(decompilerProvider, true); + ready.get(5, TimeUnit.SECONDS); + try { + return findTokenLocation(decompilerPanel, function, tokText, fieldText); + } + catch (AssertionFailedError e) { + throw e; + } + } + + 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(ProgramLocation programLocation, + DebuggerCoordinates current, FieldLocation fieldLocation, Field field) + throws Throwable { + VariableValueTable table = new VariableValueTable(); + StackUnwindWarningSet warnings = new StackUnwindWarningSet(); + waitOn(valuesService.fillVariableValueTable(table, programLocation, current, fieldLocation, + field, warnings)); + table.add(new WarningsRow(warnings)); + return table; + } + + public static final AssemblySelector NO_16BIT_CALLS = new AssemblySelector() { + @Override + public Selection 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 new Selection(res.getInstruction().fillMask(), res.getContext()); + } + throw new AssemblySemanticException(semanticErrors); + } + }; + +}