mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 08:51:05 -04:00
becb2be5bd
A syscall can hand control back to the scheduler before the instruction after it runs. SetSyscall lets the guest install its own handler for a syscall number; dispatchSyscallOverride then suspends the calling thread and queues that handler as a GuestInvocation. When the invocation finishes, EeScheduler resumes the parent thread at the address the generated code stored just before calling handleSyscall -- the instruction right after the syscall. The analyzer never marked that address as an entry point. It queues resume entries for JAL and JALR only, so no generated function could be re-entered there, EeScheduler's hasFunction() check failed, and the thread was made dormant instead of resumed. The thread simply stops; because the scheduler then drains normally and run() returns, it looks like a clean shutdown rather than a fault, which makes it awkward to recognise. This is reachable during early boot on a real title. Dragon Quest VIII hits it in crt0: the Metrowerks startup code installs a handler for syscall 0x83 and immediately issues it, and execution ends there, roughly ten functions into the binary. Note the offset is +4, not the +8 used for JAL and JALR -- syscall has no delay slot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
448 lines
19 KiB
C++
448 lines
19 KiB
C++
#include "ps2recomp/control_flow_analyzer.h"
|
|
#include "ps2recomp/types.h"
|
|
#include "ps2recomp/instructions.h"
|
|
#include "ps2recomp/control_flow_utils.h"
|
|
#include "ps2recomp/recompiler_reporter.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
|
|
namespace ps2recomp
|
|
{
|
|
ControlFlowAnalyzer::ControlFlowAnalyzer(
|
|
const std::vector<Section> §ions,
|
|
const std::unordered_map<uint32_t, std::vector<uint32_t>> &configuredJumpTableTargetsByAddress,
|
|
RecompilerReporter *reporter)
|
|
: m_sections(sections),
|
|
m_configJumpTableTargetsByAddress(configuredJumpTableTargetsByAddress),
|
|
m_reporter(reporter)
|
|
{
|
|
}
|
|
|
|
ControlFlowAnalysisResult ControlFlowAnalyzer::analyze(
|
|
const Function &function,
|
|
const std::vector<Instruction> &instructions,
|
|
const std::vector<Function> *allFunctions) const
|
|
{
|
|
ControlFlowAnalysisResult result;
|
|
std::unordered_set<uint32_t> instructionAddresses;
|
|
instructionAddresses.reserve(instructions.size());
|
|
bool hasIndirectRegisterJump = false;
|
|
std::vector<const Instruction *> indirectJumps;
|
|
|
|
auto isExecutableAddress = [&](uint32_t address) -> bool
|
|
{
|
|
for (const auto §ion : m_sections)
|
|
{
|
|
if (!section.isCode)
|
|
{
|
|
continue;
|
|
}
|
|
if (address >= section.address && address < (section.address + section.size))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
auto findContainingExternalFunction = [&](uint32_t address) -> const Function *
|
|
{
|
|
if (!allFunctions || !isExecutableAddress(address))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
const Function *best = nullptr;
|
|
for (const auto &candidateFn : *allFunctions)
|
|
{
|
|
if (!candidateFn.isRecompiled || candidateFn.isStub || candidateFn.isSkipped)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (candidateFn.name.rfind("entry_", 0) == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (address < candidateFn.start || address >= candidateFn.end)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!best || candidateFn.start > best->start)
|
|
{
|
|
best = &candidateFn;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
};
|
|
|
|
auto queueExternalEntryTarget = [&](uint32_t target)
|
|
{
|
|
const Function *containingFn = findContainingExternalFunction(target);
|
|
if (!containingFn)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (containingFn->start == function.start)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (target == containingFn->start)
|
|
{
|
|
return;
|
|
}
|
|
|
|
result.externalEntryPoints.insert(target);
|
|
};
|
|
|
|
auto queueResumeEntryTarget = [&](uint32_t resumeAddr)
|
|
{
|
|
if (resumeAddr >= function.start && resumeAddr < function.end &&
|
|
instructionAddresses.contains(resumeAddr))
|
|
{
|
|
result.entryPoints.insert(resumeAddr);
|
|
result.resumeEntryPoints.insert(resumeAddr);
|
|
}
|
|
};
|
|
|
|
auto queueLoopResumeEntryTarget = [&](uint32_t target, uint32_t sourcePc)
|
|
{
|
|
if (target > sourcePc || target == function.start)
|
|
{
|
|
return;
|
|
}
|
|
|
|
queueResumeEntryTarget(target);
|
|
};
|
|
|
|
for (const auto &inst : instructions)
|
|
{
|
|
instructionAddresses.insert(inst.address);
|
|
if (inst.opcode == OPCODE_SPECIAL &&
|
|
((inst.function == SPECIAL_JR && inst.rs != 31) ||
|
|
inst.function == SPECIAL_JALR))
|
|
{
|
|
hasIndirectRegisterJump = true;
|
|
indirectJumps.push_back(&inst);
|
|
}
|
|
}
|
|
|
|
for (const auto &inst : instructions)
|
|
{
|
|
// A syscall can hand control back to the scheduler before the
|
|
// instruction after it runs: SetSyscall lets the guest install its
|
|
// own handler, and dispatchSyscallOverride then suspends the
|
|
// calling thread and queues that handler as a GuestInvocation. When
|
|
// the invocation completes, the scheduler resumes the parent thread
|
|
// at the address the generated code stored before calling
|
|
// handleSyscall -- i.e. right here. Without an entry point there,
|
|
// EeScheduler's hasFunction() check fails and the thread is made
|
|
// dormant instead of resumed.
|
|
//
|
|
// Note the offset is +4, not the +8 used for JAL/JALR: syscall has
|
|
// no delay slot.
|
|
if (inst.opcode == OPCODE_SPECIAL && inst.function == SPECIAL_SYSCALL)
|
|
{
|
|
queueResumeEntryTarget(inst.address + 4u);
|
|
}
|
|
|
|
bool isStaticJump = (inst.opcode == OPCODE_J || inst.opcode == OPCODE_JAL);
|
|
if (inst.isBranch && inst.opcode != OPCODE_J && inst.opcode != OPCODE_JAL)
|
|
{
|
|
const int32_t offsetBytes = (static_cast<int32_t>(static_cast<int16_t>(inst.simmediate)) << 2);
|
|
const uint32_t target = static_cast<uint32_t>(
|
|
static_cast<int64_t>(inst.address + 4u) + static_cast<int64_t>(offsetBytes));
|
|
|
|
if (target >= function.start && target < function.end &&
|
|
instructionAddresses.contains(target))
|
|
{
|
|
result.entryPoints.insert(target);
|
|
queueLoopResumeEntryTarget(target, inst.address);
|
|
}
|
|
else
|
|
{
|
|
queueExternalEntryTarget(target);
|
|
}
|
|
}
|
|
else if (isStaticJump)
|
|
{
|
|
uint32_t target = buildAbsoluteJumpTarget(inst.address, inst.target);
|
|
if (target >= function.start && target < function.end &&
|
|
instructionAddresses.contains(target))
|
|
{
|
|
result.entryPoints.insert(target);
|
|
queueLoopResumeEntryTarget(target, inst.address);
|
|
|
|
if (inst.opcode == OPCODE_JAL)
|
|
{
|
|
queueResumeEntryTarget(inst.address + 8u);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
queueExternalEntryTarget(target);
|
|
|
|
if (inst.opcode == OPCODE_JAL)
|
|
{
|
|
queueResumeEntryTarget(inst.address + 8u);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasIndirectRegisterJump)
|
|
{
|
|
bool needsIndirectFallback = false;
|
|
for (const Instruction *jrInst : indirectJumps)
|
|
{
|
|
if (jrInst->function == SPECIAL_JALR)
|
|
{
|
|
queueResumeEntryTarget(jrInst->address + 8u);
|
|
}
|
|
|
|
bool foundTable = false;
|
|
|
|
uint32_t jrReg = jrInst->rs;
|
|
|
|
int lwIndex = -1;
|
|
uint32_t baseReg = 0;
|
|
int32_t lwOffset = 0;
|
|
|
|
auto it = std::find_if(instructions.begin(), instructions.end(), [&](const Instruction &inst)
|
|
{ return inst.address == jrInst->address; });
|
|
if (it != instructions.end())
|
|
{
|
|
int jrIndex = std::distance(instructions.begin(), it);
|
|
for (int i = jrIndex - 1; i >= 0 && i >= jrIndex - 20; --i)
|
|
{
|
|
const auto &inst = instructions[i];
|
|
if ((inst.opcode == OPCODE_LW || inst.opcode == OPCODE_LWU) && inst.rt == jrReg)
|
|
{
|
|
lwIndex = i;
|
|
baseReg = inst.rs;
|
|
lwOffset = inst.simmediate;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (lwIndex != -1)
|
|
{
|
|
int adduIndex = -1;
|
|
uint32_t tableBaseReg = 0;
|
|
uint32_t indexReg = 0;
|
|
for (int i = lwIndex - 1; i >= 0 && i >= lwIndex - 10; --i)
|
|
{
|
|
const auto &inst = instructions[i];
|
|
if (inst.opcode == OPCODE_SPECIAL && inst.function == SPECIAL_ADDU && inst.rd == baseReg)
|
|
{
|
|
adduIndex = i;
|
|
tableBaseReg = inst.rs;
|
|
indexReg = inst.rt;
|
|
break;
|
|
}
|
|
}
|
|
|
|
uint32_t tableAddress = 0;
|
|
bool foundTableAddress = false;
|
|
|
|
if (adduIndex != -1)
|
|
{
|
|
for (int i = adduIndex - 1; i >= 0 && i >= adduIndex - 20; --i)
|
|
{
|
|
const auto &inst = instructions[i];
|
|
if (inst.opcode == OPCODE_LUI)
|
|
{
|
|
if (inst.rt == tableBaseReg || inst.rt == indexReg)
|
|
{
|
|
uint32_t high = inst.immediate << 16;
|
|
uint32_t low = 0;
|
|
for (int j = i + 1; j < adduIndex; ++j)
|
|
{
|
|
const auto &lowInst = instructions[j];
|
|
if (lowInst.rs == inst.rt && lowInst.rt == inst.rt)
|
|
{
|
|
if (lowInst.opcode == OPCODE_ADDIU)
|
|
{
|
|
low = (uint32_t)lowInst.simmediate;
|
|
}
|
|
else if (lowInst.opcode == OPCODE_ORI)
|
|
{
|
|
low = lowInst.immediate;
|
|
}
|
|
}
|
|
}
|
|
tableAddress = high + low;
|
|
foundTableAddress = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (foundTableAddress)
|
|
{
|
|
tableAddress += lwOffset;
|
|
|
|
const auto configuredTableIt = m_configJumpTableTargetsByAddress.find(tableAddress);
|
|
if (configuredTableIt != m_configJumpTableTargetsByAddress.end())
|
|
{
|
|
std::vector<uint32_t> jrTargets;
|
|
jrTargets.reserve(configuredTableIt->second.size());
|
|
for (uint32_t target : configuredTableIt->second)
|
|
{
|
|
if (target >= function.start && target < function.end &&
|
|
instructionAddresses.contains(target))
|
|
{
|
|
jrTargets.push_back(target);
|
|
}
|
|
else
|
|
{
|
|
queueExternalEntryTarget(target);
|
|
}
|
|
}
|
|
|
|
if (!jrTargets.empty())
|
|
{
|
|
std::sort(jrTargets.begin(), jrTargets.end());
|
|
jrTargets.erase(std::unique(jrTargets.begin(), jrTargets.end()), jrTargets.end());
|
|
result.jumpTableTargets[jrInst->address] = jrTargets;
|
|
for (uint32_t target : jrTargets)
|
|
{
|
|
result.entryPoints.insert(target);
|
|
}
|
|
foundTable = true;
|
|
}
|
|
}
|
|
|
|
uint32_t unshiftedIndexReg = 0;
|
|
for (int i = adduIndex - 1; i >= 0 && i >= adduIndex - 10; --i)
|
|
{
|
|
const auto &inst = instructions[i];
|
|
if (inst.opcode == OPCODE_SPECIAL && inst.function == SPECIAL_SLL && (inst.rd == tableBaseReg || inst.rd == indexReg))
|
|
{
|
|
unshiftedIndexReg = inst.rt;
|
|
break;
|
|
}
|
|
}
|
|
|
|
uint32_t numCases = 0;
|
|
if (unshiftedIndexReg != 0)
|
|
{
|
|
for (int i = adduIndex - 1; i >= 0 && i >= adduIndex - 30; --i)
|
|
{
|
|
const auto &inst = instructions[i];
|
|
if ((inst.opcode == OPCODE_SLTIU || inst.opcode == OPCODE_SLTI) && inst.rs == unshiftedIndexReg)
|
|
{
|
|
numCases = inst.immediate;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!foundTable && numCases > 0 && numCases <= 1000)
|
|
{
|
|
const Section *rodata = nullptr;
|
|
for (const auto &sec : m_sections)
|
|
{
|
|
if (tableAddress >= sec.address && tableAddress < sec.address + sec.size)
|
|
{
|
|
rodata = &sec;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (rodata && rodata->data)
|
|
{
|
|
std::vector<uint32_t> jrTargets;
|
|
bool validJumpTable = true;
|
|
std::unordered_set<uint32_t> uniqueTargets;
|
|
for (uint32_t i = 0; i < numCases; ++i)
|
|
{
|
|
uint32_t addr = tableAddress + i * 4;
|
|
if (addr >= rodata->address && addr + 4 <= rodata->address + rodata->size)
|
|
{
|
|
uint32_t target = 0;
|
|
std::memcpy(&target, rodata->data + (addr - rodata->address), 4);
|
|
if (target >= function.start && target < function.end && instructionAddresses.contains(target))
|
|
{
|
|
if (!uniqueTargets.contains(target))
|
|
{
|
|
jrTargets.push_back(target);
|
|
uniqueTargets.insert(target);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
queueExternalEntryTarget(target);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
validJumpTable = false;
|
|
break;
|
|
}
|
|
}
|
|
if (validJumpTable && !jrTargets.empty())
|
|
{
|
|
result.jumpTableTargets[jrInst->address] = jrTargets;
|
|
for (uint32_t t : jrTargets)
|
|
{
|
|
result.entryPoints.insert(t);
|
|
}
|
|
foundTable = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Only an unresolved computed *jump* can land on an arbitrary
|
|
// instruction of this function and therefore force every address to
|
|
// become an entry point. JALR is a call: it transfers control to
|
|
// another function and comes back to the instruction after the delay
|
|
// slot, which is already queued as a resume target above. Treating a
|
|
// call like a jump here promotes the whole function for what is
|
|
// usually just a function pointer or virtual dispatch.
|
|
if (!foundTable && jrInst->function != SPECIAL_JALR)
|
|
{
|
|
needsIndirectFallback = true;
|
|
}
|
|
}
|
|
|
|
if (needsIndirectFallback)
|
|
{
|
|
if (m_reporter)
|
|
{
|
|
std::vector<uint32_t> jumpAddresses;
|
|
jumpAddresses.reserve(indirectJumps.size());
|
|
for (const Instruction *jrInst : indirectJumps)
|
|
{
|
|
jumpAddresses.push_back(jrInst->address);
|
|
}
|
|
m_reporter->recordIndirectFallbackPromotion(function.name, jumpAddresses, instructionAddresses.size());
|
|
}
|
|
|
|
for (uint32_t addr : instructionAddresses)
|
|
{
|
|
if (addr >= function.start && addr < function.end)
|
|
{
|
|
result.entryPoints.insert(addr);
|
|
// Keep labels and runtime registration for unresolved JR/JALR targets
|
|
// without emitting a local switch over every possible target.
|
|
result.indirectFallbackEntryPoints.insert(addr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
}
|