GP-3771: Add mask to the unwind analyzer (Fixes unwind with ARM/THUMB)

This commit is contained in:
Dan 2025-02-06 13:18:15 +00:00
parent d23e67a088
commit 655082ecb5
10 changed files with 387 additions and 89 deletions

View file

@ -52,6 +52,7 @@ public class ComputeUnwindInfoScript extends GhidraScript {
} }
println("Stack depth at " + currentAddress + ": " + info.depth()); println("Stack depth at " + currentAddress + ": " + info.depth());
println("Return address address: " + addressToString(info.ofReturn())); println("Return address address: " + addressToString(info.ofReturn()));
println("Return address mask: 0x" + Long.toHexString(info.maskOfReturn()));
println("Saved registers:"); println("Saved registers:");
for (Entry<Register, Address> entry : info.saved().entrySet()) { for (Entry<Register, Address> entry : info.saved().entrySet()) {
println(" " + entry); println(" " + entry);

View file

@ -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) { protected MappedLocation mapLocation(Program programOrView, Address address) {
if (programOrView instanceof TraceProgramView view) { if (programOrView instanceof TraceProgramView view) {
@ -400,7 +399,7 @@ public class VariableValueHoverService extends AbstractConfigurableHover
public CompletableFuture<VariableValueTable> fillOperand(OperandFieldLocation opLoc, public CompletableFuture<VariableValueTable> fillOperand(OperandFieldLocation opLoc,
Instruction ins) { Instruction ins) {
RefType refType = ins.getOperandRefType(opLoc.getOperandIndex()); RefType refType = ins.getOperandRefType(opLoc.getOperandIndex());
if (refType.isFlow()) { if (refType != null && refType.isFlow()) {
return null; return null;
} }
Object operand = ins.getDefaultOperandRepresentationList(opLoc.getOperandIndex()) Object operand = ins.getDefaultOperandRepresentationList(opLoc.getOperandIndex())

View file

@ -71,6 +71,11 @@ public class StackUnwindWarningSet implements Collection<StackUnwindWarning> {
return this.warnings.equals(that.warnings); return this.warnings.equals(that.warnings);
} }
@Override
public String toString() {
return this.warnings.toString();
}
@Override @Override
public int size() { public int size() {
return warnings.size(); return warnings.size();

View file

@ -96,6 +96,15 @@ sealed interface Sym {
*/ */
Sym twosComp(); 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 * Get the size of this symbol with the given compiler for context
* *
@ -144,6 +153,11 @@ sealed interface Sym {
return this; return this;
} }
@Override
public Sym and(CompilerSpec cSpec, Sym in2) {
return this;
}
@Override @Override
public Sym twosComp() { public Sym twosComp() {
return this; return this;
@ -166,19 +180,23 @@ sealed interface Sym {
public record ConstSym(long value, int size) implements Sym { public record ConstSym(long value, int size) implements Sym {
@Override @Override
public Sym add(CompilerSpec cSpec, Sym in2) { public Sym add(CompilerSpec cSpec, Sym in2) {
if (in2 instanceof ConstSym const2) { return switch (in2) {
return new ConstSym(value + const2.value, size); 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();
};
} }
if (in2 instanceof RegisterSym reg2) {
if (reg2.register() == cSpec.getStackPointer()) { @Override
return new StackOffsetSym(value); public Sym and(CompilerSpec cSpec, Sym in2) {
} return switch (in2) {
return Sym.opaque(); case ConstSym const2 -> new ConstSym(value & const2.value, size);
} case RegisterSym reg2 -> reg2.withAppliedMask(value);
if (in2 instanceof StackOffsetSym off2) { case StackDerefSym deref2 -> deref2.withAppliedMask(value);
return new StackOffsetSym(value + off2.offset); default -> Sym.opaque();
} };
return Sym.opaque();
} }
@Override @Override
@ -203,13 +221,25 @@ sealed interface Sym {
/** /**
* A register symbol * A register symbol
*/ */
public record RegisterSym(Register register) implements Sym { public record RegisterSym(Register register, long mask) implements Sym {
@Override @Override
public Sym add(CompilerSpec cSpec, Sym in2) { public Sym add(CompilerSpec cSpec, Sym in2) {
if (in2 instanceof ConstSym const2) { return switch (in2) {
return const2.add(cSpec, this); case ConstSym const2 -> const2.add(cSpec, this);
default -> Sym.opaque();
};
} }
return 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 @Override
@ -244,10 +274,18 @@ sealed interface Sym {
public record StackOffsetSym(long offset) implements Sym { public record StackOffsetSym(long offset) implements Sym {
@Override @Override
public Sym add(CompilerSpec cSpec, Sym in2) { public Sym add(CompilerSpec cSpec, Sym in2) {
if (in2 instanceof ConstSym const2) { return switch (in2) {
return new StackOffsetSym(offset + const2.value()); case ConstSym const2 -> const2.add(cSpec, this);
default -> Sym.opaque();
};
} }
return Sym.opaque();
@Override
public Sym and(CompilerSpec cSpec, Sym in2) {
return switch (in2) {
case ConstSym const2 -> const2.and(cSpec, this);
default -> Sym.opaque();
};
} }
@Override @Override
@ -276,10 +314,25 @@ sealed interface Sym {
* This represents a dereferenced {@link StackOffsetSym} (or the dereferenced stack pointer * This represents a dereferenced {@link StackOffsetSym} (or the dereferenced stack pointer
* register, in which is treated as a stack offset of 0). * 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 @Override
public Sym add(CompilerSpec cSpec, Sym in2) { 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 @Override

View file

@ -65,6 +65,8 @@ class SymPcodeArithmetic implements PcodeArithmetic<Sym> {
return in1.add(cSpec, in2); return in1.add(cSpec, in2);
case PcodeOp.INT_SUB: case PcodeOp.INT_SUB:
return in1.sub(cSpec, in2); return in1.sub(cSpec, in2);
case PcodeOp.INT_AND:
return in1.and(cSpec, in2);
default: default:
return Sym.opaque(); return Sym.opaque();
} }

View file

@ -218,17 +218,37 @@ public class SymPcodeExecutorState implements PcodeExecutorState<Sym> {
* <li>PC:Deref => location is [Stack]:PC.offset * <li>PC:Deref => location is [Stack]:PC.offset
* </ul> * </ul>
* *
* @return * @return the address (stack offset or register) of the return address
*/ */
public Address computeAddressOfReturn() { public Address computeAddressOfReturn() {
Sym expr = getVar(language.getProgramCounter(), Reason.INSPECT); return switch (getVar(language.getProgramCounter(), Reason.INSPECT)) {
if (expr instanceof StackDerefSym stackVar) { case StackDerefSym stackVar -> cSpec.getStackSpace().getAddress(stackVar.offset());
return cSpec.getStackSpace().getAddress(stackVar.offset()); case RegisterSym regVar -> regVar.register().getAddress();
default -> null;
};
} }
if (expr instanceof RegisterSym regVar) {
return regVar.register().getAddress(); /**
} * Examine this state's PC to determine how the return address is masked
return null; *
* <p>
* 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;
};
} }
/** /**

View file

@ -211,7 +211,10 @@ public class SymStateSpace {
return map.values() return map.values()
.stream() .stream()
.map(se -> se.toString(language)) .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<Address, SymEntry> subMap(Address lower, Address upper) { private NavigableMap<Address, SymEntry> subMap(Address lower, Address upper) {
@ -281,10 +284,10 @@ public class SymStateSpace {
Msg.warn(this, "Could not figure register: address=" + address + ",size=" + size); Msg.warn(this, "Could not figure register: address=" + address + ",size=" + size);
return Sym.opaque(); return Sym.opaque();
} }
return new RegisterSym(register); return new RegisterSym(register, -1);
} }
if (address.isStackAddress()) { if (address.isStackAddress()) {
return new StackDerefSym(address.getOffset(), size); return new StackDerefSym(address.getOffset(), -1, size);
} }
return Sym.opaque(); return Sym.opaque();
} }

View file

@ -98,8 +98,7 @@ public class UnwindAnalysis {
/** /**
* Wrap a {@link CodeBlock} * Wrap a {@link CodeBlock}
*/ */
record BlockVertex(CodeBlock block) { record BlockVertex(CodeBlock block) {}
}
/** /**
* Wrap a {@link CodeBlockReference} * Wrap a {@link CodeBlockReference}
@ -492,6 +491,7 @@ public class UnwindAnalysis {
new UnwindException("Cannot determine address of return pointer"); new UnwindException("Cannot determine address of return pointer");
continue; continue;
} }
long maskOfReturn = exitState.computeMaskOfReturn();
Long adjust = exitState.computeStackDepth(); Long adjust = exitState.computeStackDepth();
if (adjust == null) { if (adjust == null) {
lastError = new UnwindException("Cannot determine stack adjustment"); lastError = new UnwindException("Cannot determine stack adjustment");
@ -501,8 +501,8 @@ public class UnwindAnalysis {
warnings.addAll(exitState.warnings); warnings.addAll(exitState.warnings);
Map<Register, Address> mapByExit = exitState.computeMapUsingRegisters(); Map<Register, Address> mapByExit = exitState.computeMapUsingRegisters();
mapByExit.entrySet().retainAll(mapByEntry.entrySet()); mapByExit.entrySet().retainAll(mapByEntry.entrySet());
return new UnwindInfo(function, depth, adjust, addressOfReturn, mapByExit, return new UnwindInfo(function, depth, adjust, addressOfReturn, maskOfReturn,
new StackUnwindWarningSet(warnings), null); mapByExit, new StackUnwindWarningSet(warnings), null);
} }
} }
if (lastSuccessfulEntryState != null) { if (lastSuccessfulEntryState != null) {
@ -510,16 +510,16 @@ public class UnwindAnalysis {
try { try {
long adjust = SymPcodeExecutor.computeStackChange(function, warnings); long adjust = SymPcodeExecutor.computeStackChange(function, warnings);
return new UnwindInfo(function, lastSuccessfulEntryState.computeStackDepth(), return new UnwindInfo(function, lastSuccessfulEntryState.computeStackDepth(),
adjust, null, lastSuccessfulEntryState.computeMapUsingStack(), adjust, null, -1, lastSuccessfulEntryState.computeMapUsingStack(),
new StackUnwindWarningSet(warnings), lastError); new StackUnwindWarningSet(warnings), lastError);
} }
catch (Exception e) { catch (Exception e) {
return new UnwindInfo(function, lastSuccessfulEntryState.computeStackDepth(), return new UnwindInfo(function, lastSuccessfulEntryState.computeStackDepth(),
null, null, lastSuccessfulEntryState.computeMapUsingStack(), null, null, -1, lastSuccessfulEntryState.computeMapUsingStack(),
new StackUnwindWarningSet(warnings), e); 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( new StackUnwindWarningSet(warnings), new UnwindException(
"Could not analyze any path from %s entry to %s.\n%s".formatted(function, pc, "Could not analyze any path from %s entry to %s.\n%s".formatted(function, pc,
lastError.getMessage()), lastError.getMessage()),

View file

@ -31,12 +31,28 @@ import ghidra.util.task.TaskMonitor;
/** /**
* Information for interpreting the current stack frame and unwinding to the next * 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, public record UnwindInfo(Function function, Long depth, Long adjust, Address ofReturn,
Map<Register, Address> saved, StackUnwindWarningSet warnings, Exception error) { long maskOfReturn, Map<Register, Address> 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) { 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; return ofReturn;
} }
/**
* The mask applied to the return address
*
* <p>
* 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.
*
* <p>
* <b>NOTE</b>: 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 <em>address of</em> the return address, given a stack base * The <em>address of</em> 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 * 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 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) { public void mapSavedRegisters(Address base, SavedRegisterMap map) {
for (Entry<Register, Address> ent : saved.entrySet()) { for (Entry<Register, Address> ent : saved.entrySet()) {
@ -221,7 +260,8 @@ public record UnwindInfo(Function function, Long depth, Long adjust, Address ofR
AddressSpace codeSpace, Register pc) { AddressSpace codeSpace, Register pc) {
T value = computeNextPc(base, state, pc); T value = computeNextPc(base, state, pc);
long concrete = state.getArithmetic().toLong(value, Purpose.INSPECT); long concrete = state.getArithmetic().toLong(value, Purpose.INSPECT);
return codeSpace.getAddress(concrete); long masked = concrete & maskOfReturn;
return codeSpace.getAddress(masked);
} }
/** /**

View file

@ -38,6 +38,7 @@ import ghidra.app.decompiler.*;
import ghidra.app.decompiler.component.*; import ghidra.app.decompiler.component.*;
import ghidra.app.plugin.assembler.*; import ghidra.app.plugin.assembler.*;
import ghidra.app.plugin.assembler.sleigh.sem.*; 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.codebrowser.CodeBrowserPlugin;
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand; import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; 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.TraceBreakpoint;
import ghidra.trace.model.breakpoint.TraceBreakpointKind; import ghidra.trace.model.breakpoint.TraceBreakpointKind;
import ghidra.trace.model.listing.TraceData; import ghidra.trace.model.listing.TraceData;
import ghidra.trace.model.memory.TraceMemorySpace;
import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.schedule.Scheduler; import ghidra.trace.model.time.schedule.Scheduler;
import ghidra.util.Msg; 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 @Test
public void testComputeUnwindInfoX86_32() throws Throwable { public void testComputeUnwindInfoX86_32() throws Throwable {
addPlugin(tool, CodeBrowserPlugin.class); addPlugin(tool, CodeBrowserPlugin.class);
@ -599,13 +654,14 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerTest {
UnwindAnalysis ua = new UnwindAnalysis(program); UnwindAnalysis ua = new UnwindAnalysis(program);
UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor); UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor);
assertEquals( assertEquals(new UnwindInfo(function, 0L, 4L, stack(0), -1,
new UnwindInfo(function, 0L, 4L, stack(0), Map.of(), new StackUnwindWarningSet(), null), Map.of(), new StackUnwindWarningSet(), null),
infoAtEntry); infoAtEntry);
UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor); UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor);
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(), null), infoAtBody); Map.of(register("EBP"), stack(-4)), new StackUnwindWarningSet(), null),
infoAtBody);
} }
@Test @Test
@ -623,14 +679,15 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerTest {
UnwindAnalysis ua = new UnwindAnalysis(program); UnwindAnalysis ua = new UnwindAnalysis(program);
UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor); UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor);
assertEquals( assertEquals(new UnwindInfo(function, 0L, 4L, stack(0), -1,
new UnwindInfo(function, 0L, 4L, stack(0), Map.of(), new StackUnwindWarningSet(), null), Map.of(), new StackUnwindWarningSet(), null),
infoAtEntry); infoAtEntry);
UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor); UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor);
assertEquals( assertEquals(new UnwindInfo(function, -20L, 4L, stack(0), -1,
new UnwindInfo(function, -20L, 4L, stack(0), Map.of(register("EBP"), stack(-4)), Map.of(register("EBP"), stack(-4)),
new StackUnwindWarningSet(new UnspecifiedConventionStackUnwindWarning(myExtern), new StackUnwindWarningSet(
new UnspecifiedConventionStackUnwindWarning(myExtern),
new UnknownPurgeStackUnwindWarning(myExtern)), new UnknownPurgeStackUnwindWarning(myExtern)),
null), null),
infoAtBody); infoAtBody);
@ -649,18 +706,47 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerTest {
UnwindAnalysis ua = new UnwindAnalysis(program); UnwindAnalysis ua = new UnwindAnalysis(program);
UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor); UnwindInfo infoAtEntry = ua.computeUnwindInfo(entry, monitor);
assertEquals( assertEquals(new UnwindInfo(function, 0L, 4L, stack(0), -1,
new UnwindInfo(function, 0L, 4L, stack(0), Map.of(), new StackUnwindWarningSet(), null), Map.of(), new StackUnwindWarningSet(), null),
infoAtEntry); infoAtEntry);
UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor); UnwindInfo infoAtBody = ua.computeUnwindInfo(bodyInstr, monitor);
DataType ptr2Undef = new PointerDataType(DataType.DEFAULT, program.getDataTypeManager()); 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)), Map.of(register("EBP"), stack(-4)),
new StackUnwindWarningSet(new UnexpectedTargetTypeStackUnwindWarning(ptr2Undef)), null), new StackUnwindWarningSet(new UnexpectedTargetTypeStackUnwindWarning(ptr2Undef)), null),
infoAtBody); 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 @Test
public void testUnwindTopFrameX86_32() throws Throwable { public void testUnwindTopFrameX86_32() throws Throwable {
addPlugin(tool, CodeBrowserPlugin.class); addPlugin(tool, CodeBrowserPlugin.class);
@ -814,6 +900,95 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerTest {
assertEquals(BigInteger.valueOf(34), retVal.toBigInteger(false)); 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 <skip>;
emu_swi();
<skip>
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<WatchValue> 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 @Test
public void testCreateFramesAtEntryX86_32() throws Throwable { public void testCreateFramesAtEntryX86_32() throws Throwable {
addPlugin(tool, CodeBrowserPlugin.class); addPlugin(tool, CodeBrowserPlugin.class);