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

@ -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<Register, Address> entry : info.saved().entrySet()) {
println(" " + entry);

View file

@ -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<VariableValueTable> 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())

View file

@ -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<StackUnwindWarning> {
return this.warnings.equals(that.warnings);
}
@Override
public String toString() {
return this.warnings.toString();
}
@Override
public int size() {
return warnings.size();

View file

@ -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

View file

@ -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<Sym> {
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();
}

View file

@ -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<Sym> {
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<Sym> {
* <li>PC:Deref => location is [Stack]:PC.offset
* </ul>
*
* @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
*
* <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

@ -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<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);
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();
}

View file

@ -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<Register, Address> 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()),

View file

@ -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<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) {
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
*
* <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
*
@ -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<Register, Address> 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);
}
/**

View file

@ -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 <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
public void testCreateFramesAtEntryX86_32() throws Throwable {
addPlugin(tool, CodeBrowserPlugin.class);