Files
PS2Recomp/ps2xTest/src/elf_analyzer_tests.cpp
T
Ranieri 669114f3f6 Feature/runtime review codegen fixes (#87)
* feat: small fixes on code gen

* feat: added code gen test

* feat: rename IOP

* fix: fix special case on JR
feat: added code generator test

* feat: ps2 logs now need special macros

* feat: a lot of regressions test
feat: use test to fix bugs on runtime
fix: fix incorrect instructions on code generator
feat: added missing decode on r5900 decoder
feat: added scissor on rasterizer

* feat: better ghidra plugin analyzer
fix: fix real bug on function finding on elf analyzer

* feat: some logs on GS
feat: added more syscalls stubs
feat: added more ps2 stubs

* feat: added missing stub
2026-02-27 03:44:59 -03:00

262 lines
14 KiB
C++

#include "MiniTest.h"
#include "ps2recomp/elf_analyzer.h"
#include "ps2recomp/instructions.h"
#include "ps2recomp/types.h"
#include <unordered_map>
#include <unordered_set>
#include <vector>
using namespace ps2recomp;
namespace
{
Instruction makeInstruction(uint32_t address, uint32_t opcode)
{
Instruction inst;
inst.address = address;
inst.opcode = opcode;
return inst;
}
}
void register_elf_analyzer_tests()
{
MiniTest::Case("ElfAnalyzerHeuristics", [](TestCase &tc)
{
tc.Run("library-symbol classification table", [](TestCase &t)
{
ElfAnalyzer analyzer("dummy.elf");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("printf"),
"printf should be classified as library");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("_printf"),
"_printf should be classified as library");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("sceCdRead"),
"sce-prefixed PS2 API should be classified as library");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("SetSyscall"),
"SetSyscall kernel wrapper should be classified as library/runtime");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("SetTLBEntry"),
"SetTLBEntry kernel wrapper should be classified as library/runtime");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("InitTLB"),
"InitTLB kernel wrapper should be classified as library/runtime");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("AddIntcHandler2"),
"AddIntcHandler2 kernel wrapper should be classified as library/runtime");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("SetGsCrt"),
"SetGsCrt kernel wrapper should be classified as library/runtime");
t.IsFalse(analyzer.isLibrarySymbolNameForHeuristics("bhEne13_Brain"),
"named game function should not be classified as library");
t.IsFalse(analyzer.isLibrarySymbolNameForHeuristics("sub_00100C00"),
"unreliable auto-generated names should not be classified as library"); });
tc.Run("reliable-symbol heuristic filters autogenerated names", [](TestCase &t)
{
t.IsTrue(ElfAnalyzer::isReliableSymbolNameForHeuristics("bhEne13_Brain"),
"expected game symbol to be considered reliable");
t.IsTrue(ElfAnalyzer::isReliableSymbolNameForHeuristics("SetupSoundDriver"),
"expected named function to be considered reliable");
t.IsTrue(ElfAnalyzer::isReliableSymbolNameForHeuristics("sceCdRead"),
"expected PS2 API symbol to be considered reliable");
t.IsFalse(ElfAnalyzer::isReliableSymbolNameForHeuristics("sub_00100C00"),
"sub_ prefix should be treated as unreliable");
t.IsFalse(ElfAnalyzer::isReliableSymbolNameForHeuristics("func_1ABC"),
"func_ prefix should be treated as unreliable");
t.IsFalse(ElfAnalyzer::isReliableSymbolNameForHeuristics("entry_001000"),
"entry_ prefix should be treated as unreliable");
t.IsFalse(ElfAnalyzer::isReliableSymbolNameForHeuristics("LAB_00001234"),
"LAB_ prefix should be treated as unreliable");
t.IsFalse(ElfAnalyzer::isReliableSymbolNameForHeuristics("0x00100ABC"),
"pure hex-style symbol should be treated as unreliable"); });
tc.Run("system-symbol heuristic is strict to system patterns", [](TestCase &t)
{
t.IsTrue(ElfAnalyzer::isSystemSymbolNameForHeuristics("__main"),
"__main should be classified as system");
t.IsTrue(ElfAnalyzer::isSystemSymbolNameForHeuristics("_start"),
"_start should be classified as system");
t.IsTrue(ElfAnalyzer::isSystemSymbolNameForHeuristics(".text.startup"),
".text.* should be classified as system");
t.IsFalse(ElfAnalyzer::isSystemSymbolNameForHeuristics("bhObj001"),
"game symbol should not be classified as system");
t.IsFalse(ElfAnalyzer::isSystemSymbolNameForHeuristics("SetupSoundDriver"),
"engine/game symbol should not be classified as system");
t.IsFalse(ElfAnalyzer::isSystemSymbolNameForHeuristics("sub_00100C00"),
"unreliable names should not be considered system by this classifier"); });
tc.Run("system skip keeps forced entry names recompiled", [](TestCase &t)
{
std::unordered_set<std::string> forcedNames{"_start", "_init"};
t.IsFalse(ElfAnalyzer::shouldSkipSystemSymbolForHeuristics("_start", forcedNames),
"forced entry name _start should not be skipped");
t.IsFalse(ElfAnalyzer::shouldSkipSystemSymbolForHeuristics("_init", forcedNames),
"forced entry name _init should not be skipped");
t.IsTrue(ElfAnalyzer::shouldSkipSystemSymbolForHeuristics("__main", forcedNames),
"system symbol not marked as forced should still be skipped");
t.IsTrue(ElfAnalyzer::shouldSkipSystemSymbolForHeuristics("__divdi3", {}),
"compiler helper __divdi3 should be skippable as system/runtime");
t.IsFalse(ElfAnalyzer::shouldSkipSystemSymbolForHeuristics("ps2___divdi3", {}),
"generated ps2_ wrapper names should not be treated as system"); });
tc.Run("entry-point mapping handles exact inside and fallback", [](TestCase &t)
{
Function f1;
f1.name = "funcA";
f1.start = 0x1000;
f1.end = 0x1100;
Function f2;
f2.name = "funcB";
f2.start = 0x1100;
f2.end = 0x1200;
Function f3;
f3.name = "fallbackA";
f3.start = 0x100000;
f3.end = 0x100100;
std::vector<Function> functions{f1, f2, f3};
t.Equals(ElfAnalyzer::findEntryFunctionIndexForHeuristics(functions, 0x1100), 1,
"exact entry should map to function start");
t.Equals(ElfAnalyzer::findEntryFunctionIndexForHeuristics(functions, 0x10F0), 0,
"entry inside range should map to containing function");
t.Equals(ElfAnalyzer::findEntryFunctionIndexForHeuristics(functions, 0x2000), -1,
"unknown entry should return no mapping");
t.Equals(ElfAnalyzer::findFallbackEntryFunctionIndexForHeuristics(functions), 2,
"fallback should find 0x100000 entry");
Function fallbackB;
fallbackB.name = "fallbackB";
fallbackB.start = 0x80100000;
fallbackB.end = 0x80100100;
std::vector<Function> fallbackOnly{fallbackB};
t.Equals(ElfAnalyzer::findFallbackEntryFunctionIndexForHeuristics(fallbackOnly), 0,
"fallback should also accept 0x80100000"); });
tc.Run("signal-based skip heuristics keep reliable names and skip unreliable/system", [](TestCase &t)
{
// Hardware I/O signal via LUI upper address in I/O region.
Instruction hw = makeInstruction(0x1000, OPCODE_LUI);
hw.immediate = 0x1002; // 0x10020000
std::vector<Instruction> hwInst{hw};
const bool hasHardwareIO = ElfAnalyzer::hasHardwareIOSignalForHeuristics(hwInst);
t.IsTrue(hasHardwareIO, "hardware I/O signal should be detected");
// Large + complex MMI signal.
std::vector<Instruction> largeMmi(501);
largeMmi[250] = makeInstruction(0x2000, OPCODE_MMI);
largeMmi[250].isMMI = true;
largeMmi[250].function = MMI_MMI1;
const bool hasLargeComplexMMI = ElfAnalyzer::hasLargeComplexMMISignalForHeuristics(largeMmi);
t.IsTrue(hasLargeComplexMMI, "large complex MMI signal should be detected");
// Self-modifying signal: SW into a code section, with base from preceding LUI.
Instruction lui = makeInstruction(0x3000, OPCODE_LUI);
lui.rt = 9;
lui.immediate = 0x1000; // base 0x10000000
Instruction sw = makeInstruction(0x3004, OPCODE_SW);
sw.rs = 9;
sw.immediate = 0x2000; // target 0x10002000
std::vector<Instruction> smcInst{lui, sw};
Section code{};
code.name = ".text";
code.address = 0x10002000;
code.size = 0x100;
code.isCode = true;
std::vector<Section> sections{code};
const bool hasSelfModifying = ElfAnalyzer::hasSelfModifyingSignalForHeuristics(smcInst, sections);
t.IsTrue(hasSelfModifying, "self-modifying signal should be detected");
// Decision behavior by name reliability/system-ness.
t.IsFalse(hasHardwareIO && ElfAnalyzer::shouldAutoSkipNameForHeuristics("bhEne13_Brain"),
"reliable game symbol should not auto-skip from hardware signal alone");
t.IsTrue(hasHardwareIO && ElfAnalyzer::shouldAutoSkipNameForHeuristics("sub_00100C00"),
"unreliable symbol should auto-skip when risky signals exist");
t.IsTrue(hasLargeComplexMMI && ElfAnalyzer::shouldAutoSkipNameForHeuristics("__main"),
"system symbol should auto-skip when risky signals exist");
t.IsFalse(hasSelfModifying && ElfAnalyzer::shouldAutoSkipNameForHeuristics("topThread"),
"do-not-skip list should override auto-skip"); });
tc.Run("patch-density threshold behavior", [](TestCase &t)
{
t.IsTrue(ElfAnalyzer::shouldSkipForPatchDensityForHeuristics("sub_00100C00", 100, 6, false),
"high-density patches on unreliable names should skip");
t.IsFalse(ElfAnalyzer::shouldSkipForPatchDensityForHeuristics("sub_00100C00", 200, 6, false),
"density below threshold should not skip");
t.IsFalse(ElfAnalyzer::shouldSkipForPatchDensityForHeuristics("sub_00100C00", 100, 5, false),
"patch count <= 5 should not skip");
t.IsFalse(ElfAnalyzer::shouldSkipForPatchDensityForHeuristics("printf", 100, 6, true),
"library functions should not be auto-skipped by patch density");
t.IsFalse(ElfAnalyzer::shouldSkipForPatchDensityForHeuristics("bhEne13_Brain", 100, 6, false),
"reliable game function should not be auto-skipped by patch density");
t.IsFalse(ElfAnalyzer::shouldSkipForPatchDensityForHeuristics("topThread", 100, 6, false),
"do-not-skip names should never be auto-skipped"); });
tc.Run("jump-table detection finds canonical sltiu/bne/lw/jr pattern", [](TestCase &t)
{
// sltiu -> bne/beq bounds check -> ... -> lui/addiu base -> lw -> jr loadedReg
Instruction sltiu = makeInstruction(0x4000, OPCODE_SLTIU);
sltiu.immediate = 3; // number of entries
Instruction bne = makeInstruction(0x4004, OPCODE_BNE);
Instruction filler = makeInstruction(0x4008, OPCODE_ADDIU);
Instruction jtLui = makeInstruction(0x400C, OPCODE_LUI);
jtLui.rt = 8;
jtLui.immediate = 0x2000;
Instruction jtAddiu = makeInstruction(0x4010, OPCODE_ADDIU);
jtAddiu.rs = 8;
jtAddiu.rt = 9; // load base register
jtAddiu.immediate = 0x0100;
Instruction jtLoad = makeInstruction(0x4014, OPCODE_LW);
jtLoad.rs = 9;
jtLoad.rt = 10;
Instruction jtJump = makeInstruction(0x4018, OPCODE_SPECIAL);
jtJump.function = SPECIAL_JR;
jtJump.rs = 10;
std::vector<Instruction> instructions{sltiu, bne, filler, jtLui, jtAddiu, jtLoad, jtJump};
const uint32_t base = (0x2000u << 16) | 0x0100u;
std::unordered_map<uint32_t, uint32_t> tableMemory{
{base + 0, 0x101000},
{base + 4, 0x102000},
{base + 8, 0x103000},
};
auto readWord = [&tableMemory](uint32_t address, uint32_t &outWord) -> bool
{
auto it = tableMemory.find(address);
if (it == tableMemory.end())
{
return false;
}
outWord = it->second;
return true;
};
auto jumpTables = ElfAnalyzer::detectJumpTablesForHeuristics(instructions, std::vector<Section>(), readWord);
t.Equals(jumpTables.size(), static_cast<size_t>(1), "one jump table should be detected");
if (!jumpTables.empty())
{
t.Equals(jumpTables[0].address, base, "jump table base address should match LUI/ADDIU pattern");
t.Equals(jumpTables[0].baseRegister, static_cast<uint32_t>(9), "base register should match LW base");
t.Equals(jumpTables[0].entries.size(), static_cast<size_t>(3), "entry count should match SLTIU bound");
t.Equals(jumpTables[0].entries[0].target, static_cast<uint32_t>(0x101000), "entry 0 target should match");
t.Equals(jumpTables[0].entries[1].target, static_cast<uint32_t>(0x102000), "entry 1 target should match");
t.Equals(jumpTables[0].entries[2].target, static_cast<uint32_t>(0x103000), "entry 2 target should match");
}
Instruction invalid = sltiu;
invalid.immediate = 1001; // rejected by guard
auto invalidTables = ElfAnalyzer::detectJumpTablesForHeuristics(
std::vector<Instruction>{invalid, bne, filler, jtLui, jtAddiu, jtLoad, jtJump}, std::vector<Section>(),
readWord);
t.Equals(invalidTables.size(), static_cast<size_t>(0),
"bounds over guard limit should not produce a jump table"); }); });
}