From 655082ecb59b7a1c7266ffad8fb6a252b40efa72 Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Thu, 6 Feb 2025 13:18:15 +0000 Subject: [PATCH] GP-3771: Add mask to the unwind analyzer (Fixes unwind with ARM/THUMB) --- .../ComputeUnwindInfoScript.java | 5 +- .../stack/vars/VariableValueHoverService.java | 9 +- .../debug/stack/StackUnwindWarningSet.java | 9 +- .../app/plugin/core/debug/stack/Sym.java | 105 ++++++--- .../core/debug/stack/SymPcodeArithmetic.java | 6 +- .../debug/stack/SymPcodeExecutorState.java | 56 +++-- .../core/debug/stack/SymStateSpace.java | 13 +- .../core/debug/stack/UnwindAnalysis.java | 18 +- .../plugin/core/debug/stack/UnwindInfo.java | 52 ++++- .../core/debug/stack/StackUnwinderTest.java | 203 ++++++++++++++++-- 10 files changed, 387 insertions(+), 89 deletions(-) diff --git a/Ghidra/Debug/Debugger/ghidra_scripts/ComputeUnwindInfoScript.java b/Ghidra/Debug/Debugger/ghidra_scripts/ComputeUnwindInfoScript.java index b02b3bdd67..f7342f931a 100644 --- a/Ghidra/Debug/Debugger/ghidra_scripts/ComputeUnwindInfoScript.java +++ b/Ghidra/Debug/Debugger/ghidra_scripts/ComputeUnwindInfoScript.java @@ -4,9 +4,9 @@ * 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. @@ -52,6 +52,7 @@ public class ComputeUnwindInfoScript extends GhidraScript { } println("Stack depth at " + currentAddress + ": " + info.depth()); println("Return address address: " + addressToString(info.ofReturn())); + println("Return address mask: 0x" + Long.toHexString(info.maskOfReturn())); println("Saved registers:"); for (Entry entry : info.saved().entrySet()) { println(" " + entry); 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 64a196c75e..50d6f35765 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 @@ -4,9 +4,9 @@ * 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. @@ -261,8 +261,7 @@ public class VariableValueHoverService extends AbstractConfigurableHover } } - record MappedLocation(Program stProg, Address stAddr, Address dynAddr) { - } + record MappedLocation(Program stProg, Address stAddr, Address dynAddr) {} protected MappedLocation mapLocation(Program programOrView, Address address) { if (programOrView instanceof TraceProgramView view) { @@ -400,7 +399,7 @@ public class VariableValueHoverService extends AbstractConfigurableHover public CompletableFuture fillOperand(OperandFieldLocation opLoc, Instruction ins) { RefType refType = ins.getOperandRefType(opLoc.getOperandIndex()); - if (refType.isFlow()) { + if (refType != null && refType.isFlow()) { return null; } Object operand = ins.getDefaultOperandRepresentationList(opLoc.getOperandIndex()) 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 5c5c01226a..0565a47c5e 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 @@ -4,9 +4,9 @@ * 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. @@ -71,6 +71,11 @@ public class StackUnwindWarningSet implements Collection { return this.warnings.equals(that.warnings); } + @Override + public String toString() { + return this.warnings.toString(); + } + @Override public int size() { return warnings.size(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/Sym.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/Sym.java index 756e8d3dd2..80a939a483 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/Sym.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/Sym.java @@ -4,9 +4,9 @@ * 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. @@ -96,6 +96,15 @@ sealed interface Sym { */ Sym twosComp(); + /** + * Logical bitwise and this and another symbol with the given compiler context + * + * @param cSpec the compiler specification + * @param in2 the second symbol + * @return the resulting symbol + */ + Sym and(CompilerSpec cSpec, Sym in2); + /** * Get the size of this symbol with the given compiler for context * @@ -144,6 +153,11 @@ sealed interface Sym { return this; } + @Override + public Sym and(CompilerSpec cSpec, Sym in2) { + return this; + } + @Override public Sym twosComp() { return this; @@ -166,19 +180,23 @@ sealed interface Sym { public record ConstSym(long value, int size) implements Sym { @Override public Sym add(CompilerSpec cSpec, Sym in2) { - if (in2 instanceof ConstSym const2) { - return new ConstSym(value + const2.value, size); - } - if (in2 instanceof RegisterSym reg2) { - if (reg2.register() == cSpec.getStackPointer()) { - return new StackOffsetSym(value); - } - return Sym.opaque(); - } - if (in2 instanceof StackOffsetSym off2) { - return new StackOffsetSym(value + off2.offset); - } - return Sym.opaque(); + return switch (in2) { + case ConstSym const2 -> new ConstSym(value + const2.value, size); + case RegisterSym reg2 when reg2.register() == cSpec + .getStackPointer() -> new StackOffsetSym(value); + case StackOffsetSym off2 -> new StackOffsetSym(value + off2.offset); + default -> Sym.opaque(); + }; + } + + @Override + public Sym and(CompilerSpec cSpec, Sym in2) { + return switch (in2) { + case ConstSym const2 -> new ConstSym(value & const2.value, size); + case RegisterSym reg2 -> reg2.withAppliedMask(value); + case StackDerefSym deref2 -> deref2.withAppliedMask(value); + default -> Sym.opaque(); + }; } @Override @@ -203,13 +221,25 @@ sealed interface Sym { /** * A register symbol */ - public record RegisterSym(Register register) implements Sym { + public record RegisterSym(Register register, long mask) implements Sym { @Override public Sym add(CompilerSpec cSpec, Sym in2) { - if (in2 instanceof ConstSym const2) { - return const2.add(cSpec, this); - } - return Sym.opaque(); + return switch (in2) { + case ConstSym const2 -> const2.add(cSpec, this); + default -> Sym.opaque(); + }; + } + + @Override + public Sym and(CompilerSpec cSpec, Sym in2) { + return switch (in2) { + case ConstSym const2 -> const2.and(cSpec, this); + default -> Sym.opaque(); + }; + } + + public RegisterSym withAppliedMask(long mask) { + return new RegisterSym(register, this.mask & mask); } @Override @@ -244,10 +274,18 @@ sealed interface Sym { public record StackOffsetSym(long offset) implements Sym { @Override public Sym add(CompilerSpec cSpec, Sym in2) { - if (in2 instanceof ConstSym const2) { - return new StackOffsetSym(offset + const2.value()); - } - return Sym.opaque(); + return switch (in2) { + case ConstSym const2 -> const2.add(cSpec, this); + default -> Sym.opaque(); + }; + } + + @Override + public Sym and(CompilerSpec cSpec, Sym in2) { + return switch (in2) { + case ConstSym const2 -> const2.and(cSpec, this); + default -> Sym.opaque(); + }; } @Override @@ -276,10 +314,25 @@ sealed interface Sym { * This represents a dereferenced {@link StackOffsetSym} (or the dereferenced stack pointer * register, in which is treated as a stack offset of 0). */ - public record StackDerefSym(long offset, int size) implements Sym { + public record StackDerefSym(long offset, long mask, int size) implements Sym { @Override public Sym add(CompilerSpec cSpec, Sym in2) { - return Sym.opaque(); + return switch (in2) { + case ConstSym const2 -> const2.add(cSpec, this); + default -> Sym.opaque(); + }; + } + + @Override + public Sym and(CompilerSpec cSpec, Sym in2) { + return switch (in2) { + case ConstSym const2 -> const2.and(cSpec, this); + default -> Sym.opaque(); + }; + } + + public StackDerefSym withAppliedMask(long mask) { + return new StackDerefSym(offset, this.mask & mask, size); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymPcodeArithmetic.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymPcodeArithmetic.java index ef29bbef32..28830c75dd 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymPcodeArithmetic.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymPcodeArithmetic.java @@ -4,9 +4,9 @@ * 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. @@ -65,6 +65,8 @@ class SymPcodeArithmetic implements PcodeArithmetic { return in1.add(cSpec, in2); case PcodeOp.INT_SUB: return in1.sub(cSpec, in2); + case PcodeOp.INT_AND: + return in1.and(cSpec, in2); default: return Sym.opaque(); } 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 e04771f2a9..dc7297774a 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 @@ -4,9 +4,9 @@ * 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. @@ -79,16 +79,16 @@ public class SymPcodeExecutorState implements PcodeExecutorState { public String toString() { return String.format(""" %s[ - cSpec=%s - stack=%s - registers=%s - unique=%s + cSpec=%s + stack=%s + registers=%s + unique=%s ] """, getClass().getSimpleName(), cSpec.toString(), - stackSpace.toString(" ", language), - registerSpace.toString(" ", language), - uniqueSpace.toString(" ", language)); + stackSpace.toString(" ", language), + registerSpace.toString(" ", language), + uniqueSpace.toString(" ", language)); } @Override @@ -218,17 +218,37 @@ public class SymPcodeExecutorState implements PcodeExecutorState { *
  • PC:Deref => location is [Stack]:PC.offset * * - * @return + * @return the address (stack offset or register) of the return address */ public Address computeAddressOfReturn() { - Sym expr = getVar(language.getProgramCounter(), Reason.INSPECT); - if (expr instanceof StackDerefSym stackVar) { - return cSpec.getStackSpace().getAddress(stackVar.offset()); - } - if (expr instanceof RegisterSym regVar) { - return regVar.register().getAddress(); - } - return null; + return switch (getVar(language.getProgramCounter(), Reason.INSPECT)) { + case StackDerefSym stackVar -> cSpec.getStackSpace().getAddress(stackVar.offset()); + case RegisterSym regVar -> regVar.register().getAddress(); + default -> null; + }; + } + + /** + * Examine this state's PC to determine how the return address is masked + * + *

    + * This is only applicable in cases where {@link #computeAddressOfReturn()} returns a non-null + * address. This is to handle architectures where the low bits indicate an ISA mode, and the + * higher bits form the actual address. Often, the sleigh specifications for these processors + * will mask off those low bits when setting the PC. If that has happened, and the symbolic + * expression stored in the PC is otherwise understood to come from the stack or a register, + * this will return that mask. Most often, this will return -1, indicating that all bits are + * relevant to the actual address. If the symbolic expression does not indicate the stack or a + * register, this still returns -1. + * + * @return the mask, often -1 + */ + public long computeMaskOfReturn() { + return switch (getVar(language.getProgramCounter(), Reason.INSPECT)) { + case StackDerefSym stackVar -> stackVar.mask(); + case RegisterSym regVar -> regVar.mask(); + default -> -1; + }; } /** diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymStateSpace.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymStateSpace.java index 475ba09c84..58b00a584b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymStateSpace.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/stack/SymStateSpace.java @@ -4,9 +4,9 @@ * 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. @@ -211,7 +211,10 @@ public class SymStateSpace { return map.values() .stream() .map(se -> se.toString(language)) - .collect(Collectors.joining("\n" + indent, indent + "{", "\n" + indent + "}")); + .collect(Collectors.joining( + "\n" + indent + indent, + "{\n" + indent + indent, + "\n" + indent + "}")); } private NavigableMap subMap(Address lower, Address upper) { @@ -281,10 +284,10 @@ public class SymStateSpace { Msg.warn(this, "Could not figure register: address=" + address + ",size=" + size); return Sym.opaque(); } - return new RegisterSym(register); + return new RegisterSym(register, -1); } if (address.isStackAddress()) { - return new StackDerefSym(address.getOffset(), size); + return new StackDerefSym(address.getOffset(), -1, size); } return Sym.opaque(); } 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 9b29af9be1..5c91d186e7 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 @@ -4,9 +4,9 @@ * 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. @@ -98,8 +98,7 @@ public class UnwindAnalysis { /** * Wrap a {@link CodeBlock} */ - record BlockVertex(CodeBlock block) { - } + record BlockVertex(CodeBlock block) {} /** * Wrap a {@link CodeBlockReference} @@ -492,6 +491,7 @@ public class UnwindAnalysis { new UnwindException("Cannot determine address of return pointer"); continue; } + long maskOfReturn = exitState.computeMaskOfReturn(); Long adjust = exitState.computeStackDepth(); if (adjust == null) { lastError = new UnwindException("Cannot determine stack adjustment"); @@ -501,8 +501,8 @@ public class UnwindAnalysis { warnings.addAll(exitState.warnings); Map mapByExit = exitState.computeMapUsingRegisters(); mapByExit.entrySet().retainAll(mapByEntry.entrySet()); - return new UnwindInfo(function, depth, adjust, addressOfReturn, mapByExit, - new StackUnwindWarningSet(warnings), null); + return new UnwindInfo(function, depth, adjust, addressOfReturn, maskOfReturn, + mapByExit, new StackUnwindWarningSet(warnings), null); } } if (lastSuccessfulEntryState != null) { @@ -510,16 +510,16 @@ public class UnwindAnalysis { try { long adjust = SymPcodeExecutor.computeStackChange(function, warnings); return new UnwindInfo(function, lastSuccessfulEntryState.computeStackDepth(), - adjust, null, lastSuccessfulEntryState.computeMapUsingStack(), + adjust, null, -1, lastSuccessfulEntryState.computeMapUsingStack(), new StackUnwindWarningSet(warnings), lastError); } catch (Exception e) { return new UnwindInfo(function, lastSuccessfulEntryState.computeStackDepth(), - null, null, lastSuccessfulEntryState.computeMapUsingStack(), + null, null, -1, lastSuccessfulEntryState.computeMapUsingStack(), new StackUnwindWarningSet(warnings), e); } } - return new UnwindInfo(function, null, null, null, null, + return new UnwindInfo(function, null, null, null, -1, null, new StackUnwindWarningSet(warnings), new UnwindException( "Could not analyze any path from %s entry to %s.\n%s".formatted(function, pc, lastError.getMessage()), 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 fa3a9172df..d39503fe6f 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 @@ -4,9 +4,9 @@ * 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. @@ -31,12 +31,28 @@ import ghidra.util.task.TaskMonitor; /** * Information for interpreting the current stack frame and unwinding to the next + * + * @param function see {@link #function()} + * @param depth see {@link #depth()} + * @param adjust see {@link #adjust()} + * @param ofReturn see {@link #ofReturn()} + * @param maskOfReturn see {@link #maskOfReturn()} + * @param saved see {@link #saved()} + * @param warnings see {@link #warnings()} + * @param error see {@link #error()} */ public record UnwindInfo(Function function, Long depth, Long adjust, Address ofReturn, - Map saved, StackUnwindWarningSet warnings, Exception error) { + long maskOfReturn, Map saved, StackUnwindWarningSet warnings, + Exception error) { + /** + * Construct an error-only info + * + * @param error the error + * @return the info containing only the error + */ public static UnwindInfo errorOnly(Exception error) { - return new UnwindInfo(null, null, null, null, null, new StackUnwindWarningSet(), error); + return new UnwindInfo(null, null, null, null, -1, null, new StackUnwindWarningSet(), error); } /** @@ -88,6 +104,29 @@ public record UnwindInfo(Function function, Long depth, Long adjust, Address ofR return ofReturn; } + /** + * The mask applied to the return address + * + *

    + * This is to handle ISAs that use the low bits of addresses in jumps to indicate an ISA switch. + * Often, the code that returns from a function will apply a mask. If that is the case, this + * returns that mask. In most cases, this returns -1, which when applied as a mask has no + * effect. + * + *

    + * NOTE: There is currently no tracking of the ISA mode by the stack unwinder. First, the + * conventions for tracking that in the Sleigh specification varies from processor to processor. + * There is often custom-made handling of that bit programmed in Java for the emulator, but it's + * not generally accessible for static analysis. Second, for stack unwinding purposes, we use + * the statically disassembled code at the return address, anyway. That should already be of the + * correct ISA; if not, then we are already lost. + * + * @return the mask, often -1 + */ + public long maskOfReturn() { + return maskOfReturn; + } + /** * The address of the return address, given a stack base * @@ -172,7 +211,7 @@ public record UnwindInfo(Function function, Long depth, Long adjust, Address ofR * Add register map entries for the saved registers in this frame * * @param base the current frame's base pointer, as in {@link #computeBase(Address)} - * @param registerMap the register map of the stack to this point, to be modified + * @param map the register map of the stack to this point, to be modified */ public void mapSavedRegisters(Address base, SavedRegisterMap map) { for (Entry ent : saved.entrySet()) { @@ -221,7 +260,8 @@ public record UnwindInfo(Function function, Long depth, Long adjust, Address ofR AddressSpace codeSpace, Register pc) { T value = computeNextPc(base, state, pc); long concrete = state.getArithmetic().toLong(value, Purpose.INSPECT); - return codeSpace.getAddress(concrete); + long masked = concrete & maskOfReturn; + return codeSpace.getAddress(masked); } /** 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 3c1fd9873e..a74af4d4df 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 @@ -38,6 +38,7 @@ import ghidra.app.decompiler.*; import ghidra.app.decompiler.component.*; import ghidra.app.plugin.assembler.*; import ghidra.app.plugin.assembler.sleigh.sem.*; +import ghidra.app.plugin.core.analysis.*; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; @@ -81,6 +82,7 @@ import ghidra.trace.model.TraceLocation; import ghidra.trace.model.breakpoint.TraceBreakpoint; import ghidra.trace.model.breakpoint.TraceBreakpointKind; import ghidra.trace.model.listing.TraceData; +import ghidra.trace.model.memory.TraceMemorySpace; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.time.schedule.Scheduler; import ghidra.util.Msg; @@ -587,6 +589,59 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerTest { } } + protected Function createInfiniteRecursionProgramArm() throws Throwable { + createProgram("ARM:LE:32:v8", "default"); + intoProject(program); + try (Transaction tx = program.openTransaction("Assemble")) { + Address entry = addr(program, 0x00400000); + program.getMemory() + .createInitializedBlock(".text", entry, 0x1000, (byte) 0, monitor, false); + + Assembler asm = Assemblers.getAssembler(program.getLanguage()); + AssemblyBuffer buf = new AssemblyBuffer(asm, entry); + + Language language = asm.getLanguage(); + Register regCtx = language.getContextBaseRegister(); + Register regT = language.getRegister("T"); + RegisterValue rvDefault = new RegisterValue(regCtx, + asm.getContextAt(entry).toBigInteger(regCtx.getNumBytes())); + RegisterValue rvThumb = rvDefault.assign(regT, BigInteger.ONE); + AssemblyPatternBlock ctxThumb = AssemblyPatternBlock.fromRegisterValue(rvThumb); + + buf.emit(NumericUtilities.convertStringToBytes("00 b5")); // push {lr} + buf.emit(NumericUtilities.convertStringToBytes("00 04")); // lsl r0,r0,#0x10 + buf.emit(NumericUtilities.convertStringToBytes("00 0c")); // lsr r0,r0,#0x10 + buf.assemble("bl 0x%s".formatted(entry), ctxThumb); + bodyInstr = buf.getNext(); + buf.assemble("pop {r0}", ctxThumb); + buf.assemble("bx r0", ctxThumb); + + byte[] bytes = buf.getBytes(); + program.getMemory().setBytes(entry, bytes); + + Disassembler dis = Disassembler.getDisassembler(program, monitor, null); + dis.disassemble(entry, null, rvThumb, false); + + Function func = program.getFunctionManager() + .createFunction("recurseInfinitely", entry, + new AddressSet(entry, entry.add(bytes.length - 1)), + SourceType.USER_DEFINED); + return func; + } + } + + protected void openAndAnalyze(Function function) { + GhidraProgramUtilities.markProgramNotToAskToAnalyze(program); + programManager.openProgram(program); + waitForSwing(); + + AutoAnalysisManager analysisManager = AutoAnalysisManager.getAnalysisManager(program); + tool.executeBackgroundCommand(new AnalysisBackgroundCommand(analysisManager, true), + program); + analysisManager.reAnalyzeAll(function.getBody()); + waitForTasks(); + } + @Test public void testComputeUnwindInfoX86_32() throws Throwable { addPlugin(tool, CodeBrowserPlugin.class); @@ -599,13 +654,14 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerTest { UnwindAnalysis ua = new UnwindAnalysis(program); UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor); - assertEquals( - new UnwindInfo(function, 0L, 4L, stack(0), Map.of(), new StackUnwindWarningSet(), null), + assertEquals(new UnwindInfo(function, 0L, 4L, stack(0), -1, + Map.of(), new StackUnwindWarningSet(), null), infoAtEntry); UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor); - assertEquals(new UnwindInfo(function, -20L, 4L, stack(0), - Map.of(register("EBP"), stack(-4)), new StackUnwindWarningSet(), null), infoAtBody); + assertEquals(new UnwindInfo(function, -20L, 4L, stack(0), -1, + Map.of(register("EBP"), stack(-4)), new StackUnwindWarningSet(), null), + infoAtBody); } @Test @@ -623,16 +679,17 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerTest { UnwindAnalysis ua = new UnwindAnalysis(program); UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor); - assertEquals( - new UnwindInfo(function, 0L, 4L, stack(0), Map.of(), new StackUnwindWarningSet(), null), + assertEquals(new UnwindInfo(function, 0L, 4L, stack(0), -1, + Map.of(), new StackUnwindWarningSet(), null), infoAtEntry); UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor); - assertEquals( - new UnwindInfo(function, -20L, 4L, stack(0), Map.of(register("EBP"), stack(-4)), - new StackUnwindWarningSet(new UnspecifiedConventionStackUnwindWarning(myExtern), - new UnknownPurgeStackUnwindWarning(myExtern)), - null), + assertEquals(new UnwindInfo(function, -20L, 4L, stack(0), -1, + Map.of(register("EBP"), stack(-4)), + new StackUnwindWarningSet( + new UnspecifiedConventionStackUnwindWarning(myExtern), + new UnknownPurgeStackUnwindWarning(myExtern)), + null), infoAtBody); } @@ -649,18 +706,47 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerTest { UnwindAnalysis ua = new UnwindAnalysis(program); UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor); - assertEquals( - new UnwindInfo(function, 0L, 4L, stack(0), Map.of(), new StackUnwindWarningSet(), null), + assertEquals(new UnwindInfo(function, 0L, 4L, stack(0), -1, + Map.of(), new StackUnwindWarningSet(), null), infoAtEntry); UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor); DataType ptr2Undef = new PointerDataType(DataType.DEFAULT, program.getDataTypeManager()); - assertEquals(new UnwindInfo(function, -20L, 4L, stack(0), + assertEquals(new UnwindInfo(function, -20L, 4L, stack(0), -1, Map.of(register("EBP"), stack(-4)), new StackUnwindWarningSet(new UnexpectedTargetTypeStackUnwindWarning(ptr2Undef)), null), infoAtBody); } + @Test + public void testComputeUnwindInfoWithArmBx() throws Throwable { + addPlugin(tool, CodeBrowserPlugin.class); + addPlugin(tool, AutoAnalysisPlugin.class); + addPlugin(tool, DecompilePlugin.class); + + Function function = createInfiniteRecursionProgramArm(); + openAndAnalyze(function); + Address entry = function.getEntryPoint(); + + UnwindAnalysis ua = new UnwindAnalysis(program); + + UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor); + assertEquals(new UnwindInfo(function, 0L, 0L, register("lr").getAddress(), 0xfffffffeL, + Map.of(), new StackUnwindWarningSet( + new UnspecifiedConventionStackUnwindWarning(function), + new UnknownPurgeStackUnwindWarning(function)), + null), + infoAtEntry); + + UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor); + assertEquals(new UnwindInfo(function, -4L, 0L, stack(-4), 0xfffffffeL, + Map.of(), new StackUnwindWarningSet( + new UnspecifiedConventionStackUnwindWarning(function), + new UnknownPurgeStackUnwindWarning(function)), + null), + infoAtBody); + } + @Test public void testUnwindTopFrameX86_32() throws Throwable { addPlugin(tool, CodeBrowserPlugin.class); @@ -814,6 +900,95 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerTest { assertEquals(BigInteger.valueOf(34), retVal.toBigInteger(false)); } + @Test + public void testUnwindRecursiveArmThumb() throws Throwable { + addPlugin(tool, CodeBrowserPlugin.class); + addPlugin(tool, DebuggerListingPlugin.class); + addPlugin(tool, DisassemblerPlugin.class); + addPlugin(tool, AutoAnalysisPlugin.class); + addPlugin(tool, DecompilePlugin.class); + DebuggerEmulationService emuService = addPlugin(tool, DebuggerEmulationServicePlugin.class); + + Function function = createInfiniteRecursionProgramArm(); + openAndAnalyze(function); + Address entry = function.getEntryPoint(); + + useTrace(ProgramEmulationUtils.launchEmulationTrace(program, entry, this)); + tb.trace.release(this); + TraceThread thread = Unique.assertOne(tb.trace.getThreadManager().getAllThreads()); + traceManager.openTrace(tb.trace); + traceManager.activateThread(thread); + waitForSwing(); + + DebuggerCoordinates atSetup = traceManager.getCurrent(); + StackUnwinder unwinder = new StackUnwinder(tool, atSetup.getPlatform()); + + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, false); + Register sp = program.getCompilerSpec().getStackPointer(); + long spAtSetup = regs.getValue(0, sp).getUnsignedValue().longValueExact(); + + TraceBreakpoint bptUnwind; + try (Transaction tx = tb.startTransaction()) { + bptUnwind = tb.trace.getBreakpointManager() + .addBreakpoint("Breakpoints[0]", Lifespan.nowOn(0), entry, Set.of(), + Set.of(TraceBreakpointKind.SW_EXECUTE), true, "unwind stack"); + bptUnwind.setEmuSleigh(""" + if (%s >= 0x%x) goto ; + emu_swi(); + + emu_exec_decoded(); + """.formatted(sp, spAtSetup - 0xc)); + } + + EmulationResult result = emuService.run(atSetup.getPlatform(), atSetup.getTime(), monitor, + Scheduler.oneThread(thread)); + Msg.debug(this, "Broke after " + result.schedule()); + + traceManager.activateTime(result.schedule()); + waitForTasks(); + DebuggerCoordinates broke = traceManager.getCurrent(); + + AnalysisUnwoundFrame frameBroke = unwinder.start(broke, monitor); + assertEquals(tb.addr(0x00004ff0), frameBroke.getStackPointer()); + assertEquals(tb.addr(0x00004ff0), frameBroke.getBasePointer()); + assertEquals(tb.addr(0x00400000), frameBroke.getProgramCounter()); + + frameBroke = frameBroke.unwindNext(monitor); // Innermost hadn't pushed anything, yet + assertEquals(tb.addr(0x00004ff0), frameBroke.getStackPointer()); + assertEquals(tb.addr(0x00004ff4), frameBroke.getBasePointer()); + assertEquals(tb.addr(0x0040000a), frameBroke.getProgramCounter()); + + frameBroke = frameBroke.unwindNext(monitor); + assertEquals(tb.addr(0x00004ff4), frameBroke.getStackPointer()); + assertEquals(tb.addr(0x00004ff8), frameBroke.getBasePointer()); + assertEquals(tb.addr(0x0040000a), frameBroke.getProgramCounter()); + + frameBroke = frameBroke.unwindNext(monitor); + assertEquals(tb.addr(0x00004ff8), frameBroke.getStackPointer()); + assertEquals(tb.addr(0x00004ffc), frameBroke.getBasePointer()); + assertEquals(tb.addr(0x0040000a), frameBroke.getProgramCounter()); + + frameBroke = frameBroke.unwindNext(monitor); + assertEquals(tb.addr(0x00004ffc), frameBroke.getStackPointer()); + assertEquals(tb.addr(0x00005000), frameBroke.getBasePointer()); + assertEquals(tb.addr(0x0040000a), frameBroke.getProgramCounter()); + + frameBroke = frameBroke.unwindNext(monitor); + assertEquals(tb.addr(0x00005000), frameBroke.getStackPointer()); + assertNull(frameBroke.getBasePointer()); + assertEquals(tb.addr(0x00000000), frameBroke.getProgramCounter()); + + assertEquals(5, frameBroke.getLevel()); + + try { + frameBroke.unwindNext(monitor); + fail(); + } + catch (NoSuchElementException e) { + // pass + } + } + @Test public void testCreateFramesAtEntryX86_32() throws Throwable { addPlugin(tool, CodeBrowserPlugin.class);