Files
ppsspp/Core/MIPS/MIPSStackWalk.cpp
T
Henrik RydgårdandClaude Opus 5.5 024bed6065 Stack walk: Report a function's symbol start as its entry, not its prologue
The scan for the prologue took the addiu sp as the entry, which is past the
start when the function does something first (a lui, a move).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 11:04:57 +02:00

231 lines
7.3 KiB
C++

// Copyright (c) 2012- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include "Common/Log.h"
#include "Core/MemMap.h"
#include "Core/Debugger/SymbolMap.h"
#include "Core/MIPS/MIPSCodeUtils.h"
#include "Core/MIPS/MIPSStackWalk.h"
#define _RS ((op >> 21) & 0x1F)
#define _RT ((op >> 16) & 0x1F)
#define _RD ((op >> 11) & 0x1F)
#define _IMM16 ((signed short)(op & 0xFFFF))
#define MIPSTABLE_IMM_MASK 0xFC000000
#define MIPSTABLE_SPECIAL_MASK 0xFC00003F
namespace MIPSStackWalk {
using namespace MIPSCodeUtils;
// In the worst case, we scan this far above the pc for an entry.
const int MAX_FUNC_SIZE = 32768 * 4;
// After this we assume we're stuck.
const size_t MAX_DEPTH = 1024;
static u32 GuessEntry(u32 pc) {
SymbolInfo info;
if (g_symbolMap->GetSymbolInfo(&info, pc)) {
return info.address;
}
return INVALIDTARGET;
}
bool IsSWInstr(MIPSOpcode op) {
return (op & MIPSTABLE_IMM_MASK) == 0xAC000000;
}
bool IsAddImmInstr(MIPSOpcode op) {
return (op & MIPSTABLE_IMM_MASK) == 0x20000000 || (op & MIPSTABLE_IMM_MASK) == 0x24000000;
}
bool IsMovRegsInstr(MIPSOpcode op) {
// TODO: There are more options here. Let's assume addu for now.
if ((op & MIPSTABLE_SPECIAL_MASK) == 0x00000021) {
return _RS == 0 || _RT == 0;
}
return false;
}
bool ScanForAllocaSignature(u32 pc) {
// In God Eater Burst, for example, after 0880E750, there's what looks like an alloca().
// It's surrounded by "mov fp, sp" and "mov sp, fp", which is unlikely to be used for other reasons.
// It ought to be pretty close.
u32 stop = pc - 32 * 4;
for (; Memory::IsValidAddress(pc) && pc >= stop; pc -= 4) {
MIPSOpcode op = Memory::Read_Instruction(pc);
// We're looking for a "mov fp, sp" close by a "addiu sp, sp, -N".
if (IsMovRegsInstr(op) && _RD == MIPS_REG_FP && (_RS == MIPS_REG_SP || _RT == MIPS_REG_SP)) {
return true;
}
}
return false;
}
bool ScanForEntry(StackFrame &frame, u32 entry, u32 &ra) {
// Let's hope there are no > 0.5MB functions on the PSP, for the sake of humanity...
const u32 LONGEST_FUNCTION = 1024 * 512;
// TODO: Check if found entry is in the same symbol? Might be wrong sometimes...
if (entry != INVALIDTARGET && frame.pc == entry) {
// This happens when we're at the start of a function. Our ra is already correct.
frame.entry = entry;
// This function may consume stack, but the frame hasn't used it yet.
frame.stackSize = 0;
return true;
}
int ra_offset = -1;
// Start one instruction before the current frame pc, as that hasn't run yet.
const u32 start = frame.pc - 4;
u32 stop = entry;
if (entry == INVALIDTARGET) {
if (start >= PSP_GetUserMemoryBase()) {
stop = PSP_GetUserMemoryBase();
} else if (start >= PSP_GetKernelMemoryBase()) {
stop = PSP_GetKernelMemoryBase();
} else if (start >= PSP_GetScratchpadMemoryBase()) {
stop = PSP_GetScratchpadMemoryBase();
}
}
if (!Memory::IsValidAddress(start)) {
return false;
}
if (stop < start - LONGEST_FUNCTION) {
stop = start - LONGEST_FUNCTION;
}
for (u32 pc = start; Memory::IsValidAddress(pc) && pc >= stop; pc -= 4) {
_dbg_assert_(Memory::IsValidAddress(pc));
MIPSOpcode op = Memory::Read_Instruction(pc);
// Without a symbol, a jr ra above any prologue is most likely the end of the function before
// this one, so this is a leaf that starts after its delay slot. Scanning on would pick up
// that function's prologue, and read a bogus ra from the stack. Only the innermost frame
// has ra in the register to fall back on.
// (Right above pc, it's this function's own return, with pc in its delay slot.)
if (entry == INVALIDTARGET && pc != start && op.encoding == MIPS_MAKE_JR_RA()) {
if (ra == INVALIDTARGET) {
return false;
}
frame.entry = pc + 8;
frame.stackSize = 0;
return true;
}
// Here's where they store the ra address.
if (IsSWInstr(op) && _RT == MIPS_REG_RA && _RS == MIPS_REG_SP) {
ra_offset = _IMM16;
}
if (IsAddImmInstr(op) && _RT == MIPS_REG_SP && _RS == MIPS_REG_SP) {
// A positive imm either means alloca() or we went too far.
if (_IMM16 > 0) {
// TODO: Maybe check for any alloca() signature and bail?
continue;
}
if (ScanForAllocaSignature(pc)) {
continue;
}
// The prologue often isn't the first instruction (a lui or move can come first), so
// the function's symbol, if it has one, gives the real entry.
const u32 symbolEntry = GuessEntry(pc);
frame.entry = symbolEntry != INVALIDTARGET ? symbolEntry : pc;
frame.stackSize = -_IMM16;
if (ra_offset != -1 && Memory::IsValid4AlignedAddress(frame.sp + ra_offset)) {
ra = Memory::ReadUnchecked_U32(frame.sp + ra_offset);
}
return true;
}
}
return false;
}
bool DetermineFrameInfo(StackFrame &frame, u32 possibleEntry, u32 threadEntry, u32 &ra) {
if (ScanForEntry(frame, possibleEntry, ra)) {
// Awesome, found one that looks right.
return true;
} else if (ra != INVALIDTARGET && possibleEntry != INVALIDTARGET) {
// Let's just assume it's a leaf.
frame.entry = possibleEntry;
frame.stackSize = 0;
return true;
}
// Okay, we failed to get one. Our possibleEntry could be wrong, it often is.
// Let's just scan upward.
u32 newPossibleEntry = frame.pc > threadEntry ? threadEntry : frame.pc - MAX_FUNC_SIZE;
if (ScanForEntry(frame, newPossibleEntry, ra)) {
return true;
} else {
return false;
}
}
std::vector<StackFrame> Walk(const u32 pc, u32 ra, u32 sp, const u32 threadEntry, u32 threadStackTop) {
std::vector<StackFrame> frames;
if (!Memory::IsValidAddress(pc) || !Memory::IsValidAddress(sp) || !Memory::IsValidAddress(ra)) {
return frames;
}
StackFrame current;
current.pc = pc;
current.sp = sp;
current.entry = INVALIDTARGET;
current.stackSize = -1;
u32 prevEntry = INVALIDTARGET;
while (current.pc != threadEntry) {
if (!Memory::IsValidAddress(current.pc)) {
break;
}
u32 possibleEntry = GuessEntry(current.pc);
if (DetermineFrameInfo(current, possibleEntry, threadEntry, ra)) {
frames.push_back(current);
if (current.entry == threadEntry || GuessEntry(current.entry) == threadEntry) {
break;
}
if (current.entry == prevEntry || frames.size() >= MAX_DEPTH) {
// Recursion, means we're screwed. Let's just give up.
break;
}
prevEntry = current.entry;
current.pc = ra;
current.sp += current.stackSize;
ra = INVALIDTARGET;
current.entry = INVALIDTARGET;
current.stackSize = -1;
} else {
// Well, we got as far as we could.
current.entry = possibleEntry;
current.stackSize = 0;
frames.push_back(current);
break;
}
}
return frames;
}
};