#include "MiniTest.h" #include "ps2recomp/elf_analyzer.h" #include "ps2recomp/instructions.h" #include "ps2recomp/types.h" #include #include #include 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 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 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 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 hwInst{hw}; const bool hasHardwareIO = ElfAnalyzer::hasHardwareIOSignalForHeuristics(hwInst); t.IsTrue(hasHardwareIO, "hardware I/O signal should be detected"); // Large + complex MMI signal. std::vector 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 smcInst{lui, sw}; Section code{}; code.name = ".text"; code.address = 0x10002000; code.size = 0x100; code.isCode = true; std::vector
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 instructions{sltiu, bne, filler, jtLui, jtAddiu, jtLoad, jtJump}; const uint32_t base = (0x2000u << 16) | 0x0100u; std::unordered_map 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
(), readWord); t.Equals(jumpTables.size(), static_cast(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(9), "base register should match LW base"); t.Equals(jumpTables[0].entries.size(), static_cast(3), "entry count should match SLTIU bound"); t.Equals(jumpTables[0].entries[0].target, static_cast(0x101000), "entry 0 target should match"); t.Equals(jumpTables[0].entries[1].target, static_cast(0x102000), "entry 1 target should match"); t.Equals(jumpTables[0].entries[2].target, static_cast(0x103000), "entry 2 target should match"); } Instruction invalid = sltiu; invalid.immediate = 1001; // rejected by guard auto invalidTables = ElfAnalyzer::detectJumpTablesForHeuristics( std::vector{invalid, bne, filler, jtLui, jtAddiu, jtLoad, jtJump}, std::vector
(), readWord); t.Equals(invalidTables.size(), static_cast(0), "bounds over guard limit should not produce a jump table"); }); }); }