mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 16:59:35 -04:00
669114f3f6
* 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
262 lines
14 KiB
C++
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"); }); });
|
|
}
|