better analyzer and integrating sce-symbol-scanner (#130)

* feat: modularize elf analyzer
feat: added experimental sce symbol scanner
feat: change analyzer order
feat: small optimizations on analyzer

* feat: remove example_config.toml because its causing confusion on some people

* feat: embed sce symbol but leave optional import path
feat: killed skip function on analyzer but leave it so you can skip manual if you want

* feat: pin elfio tag

* feat: manually create string view with size

* feat: update ghidra script
This commit is contained in:
Ranieri
2026-06-06 23:37:12 -03:00
committed by GitHub
parent ed8b3ebee1
commit 7562ec14c9
21 changed files with 4137 additions and 1434 deletions
+3 -24
View File
@@ -114,33 +114,12 @@ Address binding for stripped ELFs:
Example:
```toml
[general]
input = "path/to/game.elf"
ghidra_output = ""
output = "output/"
single_file_output = true
low_memory_mode = true
output_worker_threads = 0
patch_syscalls = false
patch_cop0 = true
patch_cache = true
stubs = ["printf", "malloc", "free"]
# stripped function binding by address:
# stubs = ["sceCdRead@0x00123456", "SifLoadModule@0x00127890"]
stubs = ["sceCdRead@0x00123456", "SifLoadModule@0x00127890"]
# temporary return handlers:
# stubs = ["ret0@0x001D9410", "ret1@0x001D5BC8", "reta0@0x0024B7C0"]
stubs = ["ret0@0x001D9410", "ret1@0x001D5BC8", "reta0@0x0024B7C0"]
# mixed example:
# stubs = ["printf", "sceCdRead@0x00123456", "SifLoadModule@0x00127890"]
skip = ["abort", "exit"]
[patches]
instructions = [
{ address = "0x100004", value = "0x00000000" }
]
stubs = ["printf", "sceCdRead@0x00123456", "SifLoadModule@0x00127890"]
```
### Runtime
+17
View File
@@ -4,8 +4,23 @@ project(PS2Analyzer VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.3
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(nlohmann_json)
set(PS2ANALYZER_LIB_SOURCES
src/analysis_passes.cpp
src/elf_analysis_context.cpp
src/elf_analyzer.cpp
src/function_classifier.cpp
src/sce_symbol_scanner.cpp
src/toml_generator.cpp
)
add_library(ps2_analyzer_lib STATIC ${PS2ANALYZER_LIB_SOURCES})
@@ -13,10 +28,12 @@ add_library(ps2_analyzer_lib STATIC ${PS2ANALYZER_LIB_SOURCES})
target_include_directories(ps2_analyzer_lib PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/ps2xRecomp/include
${CMAKE_SOURCE_DIR}/ps2xRuntime/include
)
target_link_libraries(ps2_analyzer_lib PUBLIC
ps2_recomp_lib
nlohmann_json::nlohmann_json
)
add_executable(ps2_analyzer
+23 -5
View File
@@ -17,7 +17,22 @@ For commercial games where symbols are stripped, the analyzer uses a "JAL Scanne
Use this path only as a quick fallback when you do not yet have a Ghidra project. It is not the preferred workflow for retail games.
### 3. Ghidra Integration (For Retail and Stripped Games, Preferred)
### 3. SCE SDK Symbol Database (For SDK Function Names)
For stripped retail games, the analyzer can identify SCE/PS2SDK library functions from a
`sce-symbol-scanner` compatible database. A snapshot of the database is embedded in the
analyzer. Pass the directory that contains `symbols.json` and `tree.json`, or point
`PS2RECOMP_SCE_SYMBOL_DB` at that directory, only when you want to override the embedded
snapshot.
This path is meant to recover names such as CD/DVD, pad, DMA, GS, kernel, and libc SDK
functions so they can be classified before the expensive analysis passes run.
The current database was built from PS2 games with debug information, primarily the
Japanese set, and depends on samples that retained relocations. Treat the result as a
high-confidence hint rather than a complete SDK catalog: it can miss SDK variants that
were not present in the sampled games, and ambiguous matches are intentionally ignored.
### 4. Ghidra Integration (For Retail and Stripped Games, Preferred)
This is the recommended workflow for almost every commercial game:
1. Use the provided script: `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`.
2. Run it in Ghidra to export a CSV map of all functions.
@@ -29,19 +44,20 @@ This is the recommended workflow for almost every commercial game:
* Analyzes PS2 ELF binaries to extract symbols, functions, and structure
* Identifies common library functions that should be stubbed
* Flags system functions that should be skipped during recompilation
* Reports risky instruction patterns for manual review without auto-skipping functions
* Detects potential instruction patterns that may need patching
* Generates a ready-to-use TOML configuration file for PS2Recomp
## Using the Analyzer
```bash
ps2_analyzer <input_elf> <output_toml>
ps2_analyzer <input_elf> <output_toml> [sce_symbol_db_dir]
```
### Parameters:
* `input_elf`: Path to the PS2 ELF file.
* `output_toml`: Path where the generated TOML configuration will be saved.
* `sce_symbol_db_dir`: Optional override path to a directory containing `symbols.json` and `tree.json`.
## Example Workflow
1. Open `game.elf` in Ghidra.
@@ -57,8 +73,10 @@ Fallback:
## Generated Configuration
The tool creates a TOML file with the following sections:
* `[general]`: Paths to ELF and Ghidra maps.
* `stubs`: List of library functions to be replaced by C++ stubs.
* `skip`: List of functions to be ignored (entry points, initialization).
* `stubs`: Runtime-known functions to be replaced by C++ stubs or syscall handlers.
* `untracked_stubs`: Detected library-like functions without runtime handlers. This is
informational only and is ignored by the recompiler.
* `skip`: Legacy compatibility field. The analyzer no longer auto-populates it.
* `[patches]`: Individual instructions that need to be replaced (SYSCALLs, COP0, etc.).
## Limitations
@@ -0,0 +1,33 @@
#ifndef PS2RECOMP_ANALYSIS_PASSES_H
#define PS2RECOMP_ANALYSIS_PASSES_H
#include "ps2recomp/types.h"
#include <cstddef>
#include <cstdint>
#include <functional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace ps2recomp
{
class AnalysisPasses
{
public:
static bool hasHardwareIOSignal(const std::vector<Instruction> &instructions);
static bool hasLargeComplexMMISignal(const std::vector<Instruction> &instructions,
size_t largeInstructionThreshold = 500);
static bool hasSelfModifyingSignal(const std::vector<Instruction> &instructions,
const std::vector<Section> &sections);
static std::vector<JumpTable> detectJumpTables(
const std::vector<Instruction> &instructions,
const std::vector<Section> &sections,
const std::function<bool(uint32_t, uint32_t &)> &readWord);
static std::unordered_set<std::string> findRecursiveFunctions(
const std::unordered_map<std::string, std::vector<std::string>> &callGraph);
};
}
#endif // PS2RECOMP_ANALYSIS_PASSES_H
@@ -0,0 +1,33 @@
#ifndef PS2RECOMP_ELF_ANALYSIS_CONTEXT_H
#define PS2RECOMP_ELF_ANALYSIS_CONTEXT_H
#include "ps2recomp/types.h"
#include <cstddef>
#include <cstdint>
#include <unordered_map>
#include <vector>
namespace ps2recomp
{
struct ElfAnalysisContext
{
std::vector<Function> functions;
std::vector<Symbol> symbols;
std::vector<Section> sections;
std::vector<Relocation> relocations;
std::unordered_map<uint32_t, size_t> functionIndexByStart;
mutable std::unordered_map<uint32_t, std::vector<Instruction>> instructionCache;
void clear();
void buildFunctionIndex();
void clearInstructionCache();
Function *findFunction(uint32_t start);
const Function *findFunction(uint32_t start) const;
Function *findFunctionContaining(uint32_t address);
const Function *findFunctionContaining(uint32_t address) const;
};
}
#endif // PS2RECOMP_ELF_ANALYSIS_CONTEXT_H
+44 -17
View File
@@ -1,6 +1,9 @@
#ifndef PS2RECOMP_ELF_ANALYZER_H
#define PS2RECOMP_ELF_ANALYZER_H
#include "ps2recomp/elf_analysis_context.h"
#include "ps2recomp/function_classifier.h"
#include <string>
#include <vector>
#include <unordered_set>
@@ -9,6 +12,7 @@
#include <map>
#include <set>
#include <functional>
#include <cstdint>
namespace ps2recomp
{
@@ -31,21 +35,29 @@ namespace ps2recomp
explicit ElfAnalyzer(const std::string &elfPath);
~ElfAnalyzer();
public:
void setSceSymbolDatabasePath(const std::string &databasePath);
bool analyze();
bool generateToml(const std::string &outputPath);
bool importGhidraMap(const std::string &csvPath);
const std::vector<Function>& getFunctions() const;
public:
const std::vector<Function> &getFunctions() const;
public:
bool isLibrarySymbolNameForHeuristics(const std::string &name) const;
static bool isReliableSymbolNameForHeuristics(const std::string &name);
static bool isSystemSymbolNameForHeuristics(const std::string &name);
static bool shouldAutoSkipNameForHeuristics(const std::string &name);
static bool shouldSkipSystemSymbolForHeuristics(const std::string &name, const std::unordered_set<std::string> &forcedRecompileNames);
public:
static int findEntryFunctionIndexForHeuristics(const std::vector<Function> &functions, uint32_t entryAddress);
static int findFallbackEntryFunctionIndexForHeuristics(const std::vector<Function> &functions);
public:
static bool hasHardwareIOSignalForHeuristics(const std::vector<Instruction> &instructions);
static bool hasLargeComplexMMISignalForHeuristics(const std::vector<Instruction> &instructions, size_t largeInstructionThreshold = 500);
static bool hasSelfModifyingSignalForHeuristics(const std::vector<Instruction> &instructions, const std::vector<Section> &sections);
static bool shouldSkipForPatchDensityForHeuristics(const std::string &functionName, uint32_t functionSizeBytes, size_t patchCount, bool isLibraryFunction);
public:
static std::vector<JumpTable> detectJumpTablesForHeuristics(const std::vector<Instruction> &instructions, const std::vector<Section> &sections, const std::function<bool(uint32_t, uint32_t &)> &readWord);
static std::unordered_set<std::string> findRecursiveFunctionsForHeuristics(const std::unordered_map<std::string, std::vector<std::string>> &callGraph);
@@ -54,27 +66,43 @@ namespace ps2recomp
std::unique_ptr<ElfParser> m_elfParser;
std::unique_ptr<R5900Decoder> m_decoder;
std::vector<Function> m_functions;
std::vector<Symbol> m_symbols;
std::vector<Section> m_sections;
std::vector<Relocation> m_relocations;
ElfAnalysisContext m_context;
std::unordered_set<std::string> m_libFunctions;
std::unordered_set<std::string> m_skipFunctions;
std::unordered_set<std::string> m_untrackedStubFunctions;
std::unordered_set<uint32_t> m_forceRecompileStarts;
std::unordered_set<std::string> m_knownLibNames;
std::unordered_set<std::string> m_sceSdkFunctionNames;
FunctionClassifier m_classifier;
std::unordered_map<std::string, std::set<std::string>> m_functionDataUsage;
std::unordered_map<uint32_t, std::string> m_commonDataAccess;
std::map<uint32_t, uint32_t> m_patches;
std::map<uint32_t, std::string> m_patchReasons;
std::unordered_map<uint32_t, CFG> m_functionCFGs;
std::vector<JumpTable> m_jumpTables;
std::unordered_map<uint32_t, std::vector<FunctionCall>> m_functionCalls;
std::map<uint32_t, std::string> m_performanceCriticalReasons;
std::unordered_map<uint32_t, uint32_t> m_mmioByInstructionAddress;
void initializeLibraryFunctions();
std::string m_sceSymbolDatabasePath;
bool loadElf();
void buildFunctionIndex();
void decodeAllFunctionsOnce();
void classifyFunctions();
void runDataUsagePass();
void runPatchDetectionPass();
void runControlFlowPass();
void runJumpTablePass();
void runPerformancePass();
void runSignaturePass() const;
void printAnalysisSummary() const;
void discoverSceSdkSymbols();
void analyzeEntryPoint();
void analyzeLibraryFunctions();
void analyzeDataUsage();
@@ -96,7 +124,7 @@ namespace ps2recomp
void analyzeControlFlow();
void detectJumpTables();
void analyzePerformanceCriticalPaths() const;
void analyzePerformanceCriticalPaths();
void identifyRecursiveFunctions();
void analyzeRegisterUsage() const;
void analyzeFunctionSignatures() const;
@@ -107,16 +135,15 @@ namespace ps2recomp
bool identifyStringOperationPattern(const Function &func) const;
bool identifyMathPattern(const Function &func) const;
bool isSystemFunction(const std::string &name) const;
bool isLibraryFunction(const std::string &name) const;
void clearDecodedInstructionCache();
const std::vector<Instruction> &getDecodedInstructions(const Function &function) const;
std::vector<Instruction> decodeFunction(const Function &function) const;
CFG buildCFG(const Function &function) const;
std::string formatAddress(uint32_t address) const;
std::string escapeBackslashes(const std::string &path);
bool hasMMIInstructions(const Function &function) const;
bool hasVUInstructions(const Function &function) const;
bool shouldAutoSkipByHeuristic(const Function &function) const;
bool identifyFunctionType(const Function &function);
void identifyFunctionType(const Function &function) const;
void categorizeFunction(Function &function);
uint32_t getSuccessor(const Instruction &inst, uint32_t currentAddr);
bool isSelfModifyingCode(const Function &function) const;
@@ -0,0 +1,31 @@
#ifndef PS2RECOMP_FUNCTION_CLASSIFIER_H
#define PS2RECOMP_FUNCTION_CLASSIFIER_H
#include <cstdint>
#include <string>
#include <unordered_set>
namespace ps2recomp
{
class FunctionClassifier
{
public:
FunctionClassifier();
void setSceSdkFunctionNames(const std::unordered_set<std::string> *names);
bool isLibraryFunction(const std::string &name) const;
static bool hasRuntimeHandler(const std::string &name);
static bool isReliableSymbolName(const std::string &name);
static bool hasPs2ApiPrefix(const std::string &name);
private:
std::unordered_set<std::string> m_knownLibNames;
const std::unordered_set<std::string> *m_sceSdkFunctionNames = nullptr;
void initializeKnownLibraryFunctions();
static bool matchesKernelRuntimeName(const std::string &name);
};
}
#endif // PS2RECOMP_FUNCTION_CLASSIFIER_H
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
#ifndef PS2RECOMP_SCE_SYMBOL_SCANNER_H
#define PS2RECOMP_SCE_SYMBOL_SCANNER_H
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
namespace ps2recomp
{
struct Section;
struct SceSymbolMatch
{
uint32_t address = 0;
uint32_t size = 0;
std::string name;
std::string library;
std::string hash;
uint32_t variantHash = 0;
};
class SceSymbolScanner
{
public:
SceSymbolScanner();
~SceSymbolScanner();
bool loadDatabase(const std::string &databasePath);
std::vector<SceSymbolMatch> scan(const std::vector<Section> &sections) const;
const std::string &lastError() const;
private:
class Impl;
std::unique_ptr<Impl> m_impl;
};
}
#endif // PS2RECOMP_SCE_SYMBOL_SCANNER_H
@@ -0,0 +1,39 @@
#ifndef PS2RECOMP_TOML_GENERATOR_H
#define PS2RECOMP_TOML_GENERATOR_H
#include "ps2recomp/elf_analysis_context.h"
#include "ps2recomp/types.h"
#include <cstdint>
#include <map>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace ps2recomp
{
struct TomlGeneratorInput
{
const std::string &elfPath;
const ElfAnalysisContext &context;
const std::unordered_set<std::string> &libFunctions;
const std::unordered_set<std::string> &untrackedStubFunctions;
const std::unordered_map<uint32_t, uint32_t> &mmioByInstructionAddress;
const std::vector<JumpTable> &jumpTables;
const std::map<uint32_t, uint32_t> &patches;
const std::map<uint32_t, std::string> &patchReasons;
const std::map<uint32_t, std::string> &performanceCriticalReasons;
};
class TomlGenerator
{
public:
static bool generate(const TomlGeneratorInput &input, const std::string &outputPath);
private:
static std::string escapeBackslashes(const std::string &path);
};
}
#endif // PS2RECOMP_TOML_GENERATOR_H
+446
View File
@@ -0,0 +1,446 @@
#include "ps2recomp/analysis_passes.h"
#include "ps2recomp/instructions.h"
#include <algorithm>
#include <optional>
#include <utility>
namespace ps2recomp
{
bool AnalysisPasses::hasHardwareIOSignal(const std::vector<Instruction> &instructions)
{
for (const auto &inst : instructions)
{
if (inst.opcode == OPCODE_LUI)
{
const uint32_t upperAddr = inst.immediate << 16;
if ((upperAddr >= 0x10000000 && upperAddr < 0x14000000) || // I/O area
(upperAddr >= 0x1F800000 && upperAddr < 0x1F900000)) // Scratchpad RAM
{
return true;
}
}
}
return false;
}
bool AnalysisPasses::hasLargeComplexMMISignal(const std::vector<Instruction> &instructions,
size_t largeInstructionThreshold)
{
if (instructions.size() <= largeInstructionThreshold)
{
return false;
}
for (const auto &inst : instructions)
{
if (inst.isMMI &&
inst.opcode == OPCODE_MMI &&
(inst.function == MMI_MMI0 || inst.function == MMI_MMI1 ||
inst.function == MMI_MMI2 || inst.function == MMI_MMI3))
{
return true;
}
}
return false;
}
bool AnalysisPasses::hasSelfModifyingSignal(const std::vector<Instruction> &instructions,
const std::vector<Section> &sections)
{
for (size_t i = 0; i < instructions.size(); i++)
{
const auto &inst = instructions[i];
if (!(inst.opcode == OPCODE_SW || inst.opcode == OPCODE_SH ||
inst.opcode == OPCODE_SB || inst.opcode == OPCODE_SQ))
{
continue;
}
uint32_t baseAddr = 0;
for (int j = static_cast<int>(i) - 1; j >= 0 && j >= static_cast<int>(i) - 5; j--)
{
const auto &prevInst = instructions[static_cast<size_t>(j)];
if (prevInst.opcode == OPCODE_LUI && prevInst.rt == inst.rs)
{
baseAddr = prevInst.immediate << 16;
break;
}
}
if (baseAddr == 0)
{
continue;
}
const uint32_t targetAddr = baseAddr + static_cast<int16_t>(inst.immediate);
for (const auto &section : sections)
{
if (section.isCode &&
targetAddr >= section.address &&
targetAddr < section.address + section.size)
{
return true;
}
}
}
return false;
}
std::vector<JumpTable> AnalysisPasses::detectJumpTables(
const std::vector<Instruction> &instructions,
const std::vector<Section> &sections,
const std::function<bool(uint32_t, uint32_t &)> &readWord)
{
std::vector<JumpTable> jumpTables;
auto addSignedImm16 = [](uint32_t hiPart, uint16_t imm16) -> uint32_t
{
return hiPart + static_cast<uint32_t>(static_cast<int32_t>(static_cast<int16_t>(imm16)));
};
auto orUnsignedImm16 = [](uint32_t hiPart, uint16_t imm16) -> uint32_t
{
return hiPart | static_cast<uint32_t>(imm16);
};
auto looksLikeCodeTarget = [&sections](uint32_t addr) -> bool
{
if (addr == 0)
{
return false;
}
if (sections.empty())
{
return true;
}
for (const auto &section : sections)
{
if (!section.isCode)
{
continue;
}
const uint32_t sectionEnd = section.address + section.size;
if (addr >= section.address && addr < sectionEnd)
{
return true;
}
}
return false;
};
auto readJumpEntryCandidate = [&](uint32_t entryAddr, bool isLoadDouble, uint32_t &outTarget) -> bool
{
outTarget = 0;
uint32_t w0 = 0;
if (!readWord(entryAddr, w0))
{
return false;
}
if (!isLoadDouble)
{
outTarget = w0;
return true;
}
uint32_t w1 = 0;
if (!readWord(entryAddr + 4u, w1))
{
outTarget = w0;
return true;
}
const bool w0Looks = looksLikeCodeTarget(w0);
const bool w1Looks = looksLikeCodeTarget(w1);
if (w0Looks && !w1Looks)
{
outTarget = w0;
return true;
}
if (w1Looks && !w0Looks)
{
outTarget = w1;
return true;
}
outTarget = w0;
return true;
};
auto tryBuildTable = [&](uint32_t baseAddr, uint32_t baseReg, uint32_t numEntries, uint32_t strideBytes, bool isLoadDouble) -> std::optional<JumpTable>
{
JumpTable jumpTable;
jumpTable.address = baseAddr;
jumpTable.baseRegister = baseReg;
uint32_t validCodeTargets = 0;
uint32_t totalRead = 0;
for (uint32_t e = 0; e < numEntries; e++)
{
const uint32_t entryAddr = baseAddr + (e * strideBytes);
uint32_t targetAddr = 0;
if (!readJumpEntryCandidate(entryAddr, isLoadDouble, targetAddr))
{
continue;
}
totalRead++;
if (looksLikeCodeTarget(targetAddr))
{
validCodeTargets++;
}
JumpTableEntry entry;
entry.index = e;
entry.target = targetAddr;
jumpTable.entries.push_back(entry);
}
if (jumpTable.entries.empty())
{
return std::nullopt;
}
bool ok = false;
if (sections.empty())
{
ok = (totalRead >= 2);
}
else
{
ok = (validCodeTargets >= 2) &&
(totalRead >= 2) &&
(validCodeTargets * 2 >= totalRead);
}
if (!ok)
{
return std::nullopt;
}
return jumpTable;
};
for (size_t i = 0; i < instructions.size(); i++)
{
const auto &inst = instructions[i];
if (inst.opcode != OPCODE_SLTIU || i + 2 >= instructions.size())
{
continue;
}
const auto &nextInst = instructions[i + 1];
if (nextInst.opcode != OPCODE_BNE && nextInst.opcode != OPCODE_BEQ)
{
continue;
}
for (size_t j = i + 2; j < std::min(i + 10, instructions.size()); j++)
{
const auto &loadInst = instructions[j];
const bool isLoadWord = (loadInst.opcode == OPCODE_LW);
const bool isLoadDouble = (loadInst.opcode == OPCODE_LD);
if ((!isLoadWord && !isLoadDouble) || j + 1 >= instructions.size())
{
continue;
}
const auto &jumpInst = instructions[j + 1];
if (jumpInst.opcode != OPCODE_SPECIAL ||
jumpInst.function != SPECIAL_JR ||
jumpInst.rs != loadInst.rt)
{
continue;
}
const uint32_t numEntries = inst.immediate;
if (numEntries == 0 || numEntries >= 1000)
{
break;
}
uint32_t baseAddr = 0;
for (int k = static_cast<int>(j) - 1; k >= static_cast<int>(i); k--)
{
const auto &addrInst = instructions[static_cast<size_t>(k)];
if (addrInst.opcode != OPCODE_LUI)
{
continue;
}
const uint32_t hiPart = (addrInst.immediate << 16);
if (static_cast<size_t>(k + 1) < instructions.size())
{
const auto &offsetInst = instructions[static_cast<size_t>(k + 1)];
const bool isAddiuOrOri = (offsetInst.opcode == OPCODE_ADDIU || offsetInst.opcode == OPCODE_ORI);
if (isAddiuOrOri &&
offsetInst.rs == addrInst.rt &&
offsetInst.rt == loadInst.rs)
{
if (offsetInst.opcode == OPCODE_ADDIU)
{
baseAddr = addSignedImm16(hiPart, offsetInst.immediate);
}
else
{
baseAddr = orUnsignedImm16(hiPart, offsetInst.immediate);
}
break;
}
}
if (addrInst.rt == loadInst.rs)
{
baseAddr = addSignedImm16(hiPart, loadInst.immediate);
break;
}
}
if (baseAddr == 0)
{
break;
}
const uint32_t preferredStride = isLoadDouble ? 8u : 4u;
std::optional<JumpTable> table = tryBuildTable(baseAddr, loadInst.rs, numEntries, preferredStride, isLoadDouble);
if (!table && isLoadDouble)
{
table = tryBuildTable(baseAddr, loadInst.rs, numEntries, 4u, isLoadDouble);
}
if (table)
{
jumpTables.push_back(std::move(*table));
}
break;
}
}
return jumpTables;
}
std::unordered_set<std::string> AnalysisPasses::findRecursiveFunctions(
const std::unordered_map<std::string, std::vector<std::string>> &callGraph)
{
std::unordered_set<std::string> nodes;
for (const auto &[caller, callees] : callGraph)
{
nodes.insert(caller);
for (const auto &callee : callees)
{
nodes.insert(callee);
}
}
std::unordered_map<std::string, int> index;
std::unordered_map<std::string, int> lowlink;
std::unordered_set<std::string> onStack;
std::vector<std::string> stack;
index.reserve(nodes.size());
lowlink.reserve(nodes.size());
onStack.reserve(nodes.size());
stack.reserve(nodes.size());
int currentIndex = 0;
std::vector<std::vector<std::string>> sccs;
sccs.reserve(nodes.size());
std::function<void(const std::string &)> strongconnect;
strongconnect = [&](const std::string &v)
{
index[v] = currentIndex;
lowlink[v] = currentIndex;
currentIndex++;
stack.push_back(v);
onStack.insert(v);
auto it = callGraph.find(v);
if (it != callGraph.end())
{
for (const auto &w : it->second)
{
if (!index.contains(w))
{
strongconnect(w);
lowlink[v] = std::min(lowlink[v], lowlink[w]);
}
else if (onStack.contains(w))
{
lowlink[v] = std::min(lowlink[v], index[w]);
}
}
}
if (lowlink[v] == index[v])
{
std::vector<std::string> scc;
while (!stack.empty())
{
std::string w = stack.back();
stack.pop_back();
onStack.erase(w);
scc.push_back(w);
if (w == v)
{
break;
}
}
sccs.push_back(std::move(scc));
}
};
for (const auto &name : nodes)
{
if (!index.contains(name))
{
strongconnect(name);
}
}
std::unordered_set<std::string> recursive;
for (const auto &scc : sccs)
{
if (scc.size() > 1)
{
recursive.insert(scc.begin(), scc.end());
continue;
}
const std::string &name = scc[0];
auto it = callGraph.find(name);
if (it == callGraph.end())
{
continue;
}
if (std::find(it->second.begin(), it->second.end(), name) != it->second.end())
{
recursive.insert(name);
}
}
return recursive;
}
}
+17 -2
View File
@@ -6,9 +6,11 @@ void printUsage()
{
std::cout << "PS2 ELF Analyzer\n";
std::cout << "A tool to analyze PS2 ELF files and generate TOML configuration for PS2Recomp\n\n";
std::cout << "Usage: ps2_analyzer <input_elf> <output_toml>\n";
std::cout << "Usage: ps2_analyzer <input_elf> <output_toml> [sce_symbol_db_dir]\n";
std::cout << " input_elf Path to the PS2 ELF file\n";
std::cout << " output_toml Path to output TOML configuration file\n";
std::cout << " sce_symbol_db_dir Optional override directory containing symbols.json and tree.json\n";
std::cout << " If omitted, the embedded SCE symbol database is used\n";
}
int main(int argc, char *argv[])
@@ -21,15 +23,28 @@ int main(int argc, char *argv[])
std::string elfPath = argv[1];
std::string tomlPath = argv[2];
std::string sceSymbolDbPath = argc >= 4 ? argv[3] : "";
std::cout << "PS2 ELF Analyzer\n";
std::cout << "----------------\n";
std::cout << "Input ELF: " << elfPath << "\n";
std::cout << "Output TOML: " << tomlPath << "\n\n";
if (!sceSymbolDbPath.empty())
{
std::cout << "SCE symbol DB: " << sceSymbolDbPath << "\n\n";
}
else
{
std::cout << "SCE symbol DB: embedded\n\n";
}
try
{
ps2recomp::ElfAnalyzer analyzer(elfPath);
if (!sceSymbolDbPath.empty())
{
analyzer.setSceSymbolDatabasePath(sceSymbolDbPath);
}
if (!analyzer.analyze())
{
@@ -55,4 +70,4 @@ int main(int argc, char *argv[])
std::cerr << "Error: " << e.what() << "\n";
return 1;
}
}
}
+83
View File
@@ -0,0 +1,83 @@
#include "ps2recomp/elf_analysis_context.h"
#include <algorithm>
namespace ps2recomp
{
void ElfAnalysisContext::clear()
{
functions.clear();
symbols.clear();
sections.clear();
relocations.clear();
functionIndexByStart.clear();
instructionCache.clear();
}
void ElfAnalysisContext::buildFunctionIndex()
{
functionIndexByStart.clear();
functionIndexByStart.reserve(functions.size());
for (size_t index = 0; index < functions.size(); ++index)
{
functionIndexByStart[functions[index].start] = index;
}
}
void ElfAnalysisContext::clearInstructionCache()
{
instructionCache.clear();
for (auto &func : functions)
{
func.instructions.clear();
}
}
Function *ElfAnalysisContext::findFunction(uint32_t start)
{
const auto it = functionIndexByStart.find(start);
if (it == functionIndexByStart.end())
{
return nullptr;
}
return &functions[it->second];
}
const Function *ElfAnalysisContext::findFunction(uint32_t start) const
{
const auto it = functionIndexByStart.find(start);
if (it == functionIndexByStart.end())
{
return nullptr;
}
return &functions[it->second];
}
Function *ElfAnalysisContext::findFunctionContaining(uint32_t address)
{
auto it = std::find_if(functions.begin(), functions.end(),
[address](const Function &function)
{
return function.start <= address && address < function.end;
});
if (it == functions.end())
{
return nullptr;
}
return &(*it);
}
const Function *ElfAnalysisContext::findFunctionContaining(uint32_t address) const
{
auto it = std::find_if(functions.begin(), functions.end(),
[address](const Function &function)
{
return function.start <= address && address < function.end;
});
if (it == functions.end())
{
return nullptr;
}
return &(*it);
}
}
File diff suppressed because it is too large Load Diff
+218
View File
@@ -0,0 +1,218 @@
#include "ps2recomp/function_classifier.h"
#include "ps2_runtime_calls.h"
#include <cctype>
#include <regex>
#include <vector>
namespace ps2recomp
{
FunctionClassifier::FunctionClassifier()
{
initializeKnownLibraryFunctions();
}
void FunctionClassifier::setSceSdkFunctionNames(const std::unordered_set<std::string> *names)
{
m_sceSdkFunctionNames = names;
}
bool FunctionClassifier::hasRuntimeHandler(const std::string &name)
{
return !ps2_runtime_calls::resolveSyscallName(name).empty() ||
!ps2_runtime_calls::resolveStubName(name).empty();
}
void FunctionClassifier::initializeKnownLibraryFunctions()
{
const std::vector<std::string> stdLibFuncs = {
"printf", "sprintf", "snprintf", "fprintf", "vprintf", "vfprintf", "vsprintf", "vsnprintf",
"puts", "putchar", "getchar", "gets", "fgets", "fputs", "scanf", "fscanf", "sscanf",
"sprint", "sbprintf",
"malloc", "free", "calloc", "realloc", "aligned_alloc", "posix_memalign",
"memcpy", "memset", "memmove", "memcmp", "memchr", "bcopy", "bzero",
"strcpy", "strncpy", "strcat", "strncat", "strcmp", "strncmp", "strlen", "strstr",
"strchr", "strrchr", "strdup", "strtok", "strtok_r", "strerror",
"fopen", "fclose", "fread", "fwrite", "fseek", "ftell", "rewind", "fflush",
"fgetc", "fgets", "feof", "ferror", "clearerr", "fileno", "tmpfile", "remove", "rename",
"open", "close", "read", "write", "lseek", "stat", "fstat",
"atoi", "atol", "atoll", "atof", "strtol", "strtoul", "strtoll", "strtoull", "strtod", "strtof",
"rand", "srand", "random", "srandom", "drand48", "sqrt", "pow", "exp", "log", "log10",
"sin", "cos", "tan", "asin", "acos", "atan", "atan2", "sinh", "cosh", "tanh",
"floor", "ceil", "fabs", "fmod", "frexp", "ldexp", "modf",
"time", "ctime", "clock", "difftime", "mktime", "localtime", "gmtime", "asctime", "strftime",
"gettimeofday", "nanosleep", "usleep",
"abort", "exit", "_exit", "atexit", "system", "getpid", "fork", "waitpid",
"qsort", "bsearch", "abs", "div", "labs", "ldiv", "llabs", "lldiv",
"isalnum", "isalpha", "isdigit", "islower", "isupper", "isspace", "tolower", "toupper",
"setjmp", "longjmp", "getenv", "setenv", "unsetenv",
"perror", "fputc", "getc", "ungetc", "freopen", "setvbuf", "setbuf",
"strnlen", "strspn", "strcspn", "strcasecmp", "strncasecmp"};
m_knownLibNames.insert(stdLibFuncs.begin(), stdLibFuncs.end());
}
bool FunctionClassifier::hasPs2ApiPrefix(const std::string &name)
{
if (name.empty())
{
return false;
}
const std::vector<std::string> libraryPrefixes = {
"sce", "Sce", "SCE",
"sif", "Sif", "SIF",
"gs", "Gs", "GS",
"dma", "Dma", "DMA",
"iop", "Iop", "IOP",
"vif", "Vif", "VIF",
"spu", "Spu", "SPU",
"mc", "Mc", "MC",
"libc", "Libc", "LIBC"};
std::string base = name;
if (base[0] == '_' && base.size() > 1)
{
base = base.substr(1);
}
auto hasSdkPrefixShape = [](const std::string &value, const std::string &prefix) -> bool
{
if (value.rfind(prefix, 0) != 0)
{
return false;
}
if (value.size() == prefix.size())
{
return true;
}
return !std::islower(static_cast<unsigned char>(value[prefix.size()]));
};
for (const auto &prefix : libraryPrefixes)
{
if (hasSdkPrefixShape(base, prefix))
{
return true;
}
}
return false;
}
bool FunctionClassifier::matchesKernelRuntimeName(const std::string &name)
{
if (name.empty())
{
return false;
}
static const std::regex kernelRuntimePattern(
"^(?:(?:Create|Delete|Start|ExitDelete|Exit|Terminate|Suspend|Resume|Sleep|Wakeup|CancelWakeup|Change|Rotate|Release|Setup|Register|Query|Get|Set|Refer|Poll|Wait|Signal|Enable|Disable|Flush|Reset|Add|Init)(?:Thread|Sema|EventFlag|Alarm|Intc|IntcHandler2|Dmac|DmacHandler2|OsdConfigParam|MemorySize|VSyncFlag|Heap|TLS|Status|Cache|Syscall|TLB|TLBEntry|GsCrt)|EndOfHeap|GsGetIMR|GsPutIMR|Deci2Call|Sif[A-Za-z0-9_]+|i(?:SignalSema|PollSema|ReferSemaStatus|SetEventFlag|ClearEventFlag|PollEventFlag|ReferEventFlagStatus|WakeupThread|CancelWakeupThread|ReleaseWaitThread|SetAlarm|CancelAlarm|FlushCache|sceSifSetDma|sceSifSetDChain))$");
return std::regex_match(name, kernelRuntimePattern);
}
bool FunctionClassifier::isReliableSymbolName(const std::string &name)
{
if (name.empty())
{
return false;
}
auto startsWith = [&](const char *prefix) -> bool
{
return name.rfind(prefix, 0) == 0;
};
if (startsWith("sub_") || startsWith("FUN_") || startsWith("func_") ||
startsWith("entry_") || startsWith("function_") || startsWith("LAB_"))
{
return false;
}
bool hasAlpha = false;
bool allHexOrPrefix = true;
for (char c : name)
{
if (std::isalpha(static_cast<unsigned char>(c)))
{
hasAlpha = true;
}
if (!(std::isxdigit(static_cast<unsigned char>(c)) || c == 'x' || c == 'X' || c == '_'))
{
allHexOrPrefix = false;
}
}
if (!hasAlpha)
{
return false;
}
if ((startsWith("0x") || startsWith("0X")) && allHexOrPrefix)
{
return false;
}
return true;
}
bool FunctionClassifier::isLibraryFunction(const std::string &name) const
{
if (name.empty())
{
return false;
}
if (!isReliableSymbolName(name))
{
return false;
}
if (hasRuntimeHandler(name))
{
return true;
}
std::string normalizedName = name;
if (normalizedName[0] == '_' && normalizedName.size() > 1)
{
normalizedName = normalizedName.substr(1);
}
if (hasRuntimeHandler(normalizedName))
{
return true;
}
if (m_sceSdkFunctionNames != nullptr &&
(m_sceSdkFunctionNames->contains(name) ||
m_sceSdkFunctionNames->contains(normalizedName)))
{
return true;
}
if (matchesKernelRuntimeName(normalizedName))
{
return true;
}
if (m_knownLibNames.contains(name) ||
m_knownLibNames.contains(normalizedName))
{
return true;
}
if (hasPs2ApiPrefix(name))
{
return true;
}
static const std::regex cLibPattern("^_*(mem|str|time|f?printf|f?scanf|malloc|free|calloc|realloc|atoi|itoa|rand|srand|abort|exit|atexit|getenv|system|bsearch|qsort|abs|labs|div|ldiv|mblen|mbtowc|wctomb|mbstowcs|wcstombs).*");
return std::regex_match(normalizedName, cLibPattern);
}
}
+793
View File
@@ -0,0 +1,793 @@
#include "ps2recomp/sce_symbol_scanner.h"
#include "ps2recomp/sce_symbol_database_data.h"
#include "ps2recomp/types.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <array>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <limits>
#include <map>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string_view>
#include <unordered_map>
namespace fs = std::filesystem;
namespace ps2recomp
{
namespace
{
enum class RelocationType
{
None,
Mips26,
MipsLo16,
MipsHi16,
Mips32,
MipsGpRel16,
MipsLiteral,
};
struct MatchSymbolKey
{
std::string library;
std::string name;
std::string hash;
uint32_t variantHash = 0;
};
struct RelocationRecord
{
uint32_t offset = 0;
RelocationType type = RelocationType::None;
};
struct SymbolRecord
{
std::string library;
std::string name;
std::string hashText;
std::array<uint8_t, 20> hash = {};
uint32_t variantHash = 0;
uint32_t size = 0;
bool isFunction = false;
std::vector<RelocationRecord> relocations;
size_t staticBitCount() const
{
size_t relocatedStaticBits = 0;
for (const auto &relocation : relocations)
{
switch (relocation.type)
{
case RelocationType::None:
relocatedStaticBits += 32;
break;
case RelocationType::Mips26:
relocatedStaticBits += 6;
break;
case RelocationType::MipsLo16:
case RelocationType::MipsHi16:
case RelocationType::MipsGpRel16:
case RelocationType::MipsLiteral:
relocatedStaticBits += 16;
break;
case RelocationType::Mips32:
break;
}
}
const size_t totalBits = static_cast<size_t>(size) * 8;
if (relocatedStaticBits >= totalBits)
{
return 0;
}
return totalBits - relocatedStaticBits;
}
};
struct MatchNode;
struct MatchEdge
{
uint32_t value = 0;
RelocationType relocationType = RelocationType::None;
std::unique_ptr<MatchNode> child;
};
struct MatchNode
{
uint32_t offset = 0;
std::vector<MatchEdge> next;
std::vector<MatchSymbolKey> symbols;
};
struct Candidate
{
const SymbolRecord *symbol = nullptr;
uint32_t address = 0;
uint32_t actualSize = 0;
};
static std::string toUpperAscii(std::string value)
{
for (char &ch : value)
{
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
}
return value;
}
static RelocationType parseRelocationType(const std::string &value)
{
const std::string upper = toUpperAscii(value);
if (upper == "NONE")
{
return RelocationType::None;
}
if (upper == "MIPS_26" || upper == "MIPS26")
{
return RelocationType::Mips26;
}
if (upper == "LO16" || upper == "MIPS_LO16" || upper == "MIPSLO16")
{
return RelocationType::MipsLo16;
}
if (upper == "HI16" || upper == "MIPS_HI16" || upper == "MIPSHI16")
{
return RelocationType::MipsHi16;
}
if (upper == "MIPS_32" || upper == "MIPS32")
{
return RelocationType::Mips32;
}
if (upper == "MIPS_GPREL16" || upper == "MIPSGPREL16")
{
return RelocationType::MipsGpRel16;
}
if (upper == "MIPS_LITERAL" || upper == "MIPSLITERAL")
{
return RelocationType::MipsLiteral;
}
return RelocationType::None;
}
static uint32_t relocationMask(RelocationType type)
{
switch (type)
{
case RelocationType::None:
return 0xFFFFFFFFu;
case RelocationType::Mips26:
return 0xFC000000u;
case RelocationType::MipsLo16:
case RelocationType::MipsHi16:
case RelocationType::MipsGpRel16:
case RelocationType::MipsLiteral:
return 0xFFFF0000u;
case RelocationType::Mips32:
return 0u;
}
return 0xFFFFFFFFu;
}
static uint32_t readLe32(const uint8_t *data)
{
return static_cast<uint32_t>(data[0]) |
(static_cast<uint32_t>(data[1]) << 8) |
(static_cast<uint32_t>(data[2]) << 16) |
(static_cast<uint32_t>(data[3]) << 24);
}
static void writeLe32(uint8_t *data, uint32_t value)
{
data[0] = static_cast<uint8_t>(value & 0xFFu);
data[1] = static_cast<uint8_t>((value >> 8) & 0xFFu);
data[2] = static_cast<uint8_t>((value >> 16) & 0xFFu);
data[3] = static_cast<uint8_t>((value >> 24) & 0xFFu);
}
static uint32_t disabledRelocationValue(RelocationType type, uint32_t value)
{
switch (type)
{
case RelocationType::None:
return value;
case RelocationType::Mips26:
return value & 0xFC000000u;
case RelocationType::MipsLo16:
case RelocationType::MipsHi16:
case RelocationType::MipsGpRel16:
case RelocationType::MipsLiteral:
return value & 0xFFFF0000u;
case RelocationType::Mips32:
return 0u;
}
return value;
}
static std::string toHex8(uint32_t value)
{
std::ostringstream stream;
stream << std::hex;
stream.width(8);
stream.fill('0');
stream << value;
return stream.str();
}
static std::string makeSymbolKey(const std::string &library,
const std::string &name,
const std::string &hash,
uint32_t variantHash)
{
return library + '\n' + name + '\n' + hash + '\n' + toHex8(variantHash);
}
static std::string makeSymbolKey(const SymbolRecord &symbol)
{
return makeSymbolKey(symbol.library, symbol.name, symbol.hashText, symbol.variantHash);
}
static uint8_t hexNibble(char ch)
{
if (ch >= '0' && ch <= '9')
{
return static_cast<uint8_t>(ch - '0');
}
if (ch >= 'a' && ch <= 'f')
{
return static_cast<uint8_t>(10 + ch - 'a');
}
if (ch >= 'A' && ch <= 'F')
{
return static_cast<uint8_t>(10 + ch - 'A');
}
throw std::runtime_error("invalid hex digit");
}
static std::array<uint8_t, 20> parseSha1(const std::string &hex)
{
if (hex.size() != 40)
{
throw std::runtime_error("invalid SHA-1 length");
}
std::array<uint8_t, 20> bytes = {};
for (size_t i = 0; i < bytes.size(); ++i)
{
bytes[i] = static_cast<uint8_t>((hexNibble(hex[i * 2]) << 4) |
hexNibble(hex[i * 2 + 1]));
}
return bytes;
}
static uint32_t rotateLeft(uint32_t value, uint32_t bits)
{
return (value << bits) | (value >> (32 - bits));
}
static std::array<uint8_t, 20> sha1(const std::vector<uint8_t> &data)
{
std::vector<uint8_t> message = data;
const uint64_t bitLength = static_cast<uint64_t>(message.size()) * 8u;
message.push_back(0x80u);
while ((message.size() % 64) != 56)
{
message.push_back(0u);
}
for (int shift = 56; shift >= 0; shift -= 8)
{
message.push_back(static_cast<uint8_t>((bitLength >> shift) & 0xFFu));
}
uint32_t h0 = 0x67452301u;
uint32_t h1 = 0xEFCDAB89u;
uint32_t h2 = 0x98BADCFEu;
uint32_t h3 = 0x10325476u;
uint32_t h4 = 0xC3D2E1F0u;
for (size_t chunk = 0; chunk < message.size(); chunk += 64)
{
std::array<uint32_t, 80> w = {};
for (size_t i = 0; i < 16; ++i)
{
const size_t base = chunk + i * 4;
w[i] = (static_cast<uint32_t>(message[base]) << 24) |
(static_cast<uint32_t>(message[base + 1]) << 16) |
(static_cast<uint32_t>(message[base + 2]) << 8) |
static_cast<uint32_t>(message[base + 3]);
}
for (size_t i = 16; i < 80; ++i)
{
w[i] = rotateLeft(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
}
uint32_t a = h0;
uint32_t b = h1;
uint32_t c = h2;
uint32_t d = h3;
uint32_t e = h4;
for (size_t i = 0; i < 80; ++i)
{
uint32_t f = 0;
uint32_t k = 0;
if (i < 20)
{
f = (b & c) | ((~b) & d);
k = 0x5A827999u;
}
else if (i < 40)
{
f = b ^ c ^ d;
k = 0x6ED9EBA1u;
}
else if (i < 60)
{
f = (b & c) | (b & d) | (c & d);
k = 0x8F1BBCDCu;
}
else
{
f = b ^ c ^ d;
k = 0xCA62C1D6u;
}
const uint32_t temp = rotateLeft(a, 5) + f + e + k + w[i];
e = d;
d = c;
c = rotateLeft(b, 30);
b = a;
a = temp;
}
h0 += a;
h1 += b;
h2 += c;
h3 += d;
h4 += e;
}
const std::array<uint32_t, 5> words = {h0, h1, h2, h3, h4};
std::array<uint8_t, 20> digest = {};
for (size_t i = 0; i < words.size(); ++i)
{
digest[i * 4] = static_cast<uint8_t>((words[i] >> 24) & 0xFFu);
digest[i * 4 + 1] = static_cast<uint8_t>((words[i] >> 16) & 0xFFu);
digest[i * 4 + 2] = static_cast<uint8_t>((words[i] >> 8) & 0xFFu);
digest[i * 4 + 3] = static_cast<uint8_t>(words[i] & 0xFFu);
}
return digest;
}
static fs::path resolveDatabasePath(const fs::path &inputPath)
{
if (fs::exists(inputPath / "symbols.json") && fs::exists(inputPath / "tree.json"))
{
return inputPath;
}
const fs::path resourcePath = inputPath / "symboldb" / "app" / "src" / "main" / "resources";
if (fs::exists(resourcePath / "symbols.json") && fs::exists(resourcePath / "tree.json"))
{
return resourcePath;
}
return inputPath;
}
template <size_t N>
static std::string joinJsonChunks(const std::string_view (&chunks)[N])
{
size_t size = 0;
for (std::string_view chunk : chunks)
{
size += chunk.size();
}
std::string joined;
joined.reserve(size);
for (std::string_view chunk : chunks)
{
joined.append(chunk.data(), chunk.size());
}
return joined;
}
}
class SceSymbolScanner::Impl
{
public:
bool loadDatabase(const std::string &databasePath)
{
m_lastError.clear();
m_symbols.clear();
m_root.reset();
try
{
if (databasePath.empty())
{
loadEmbeddedSymbols();
loadEmbeddedTree();
}
else
{
const fs::path resolvedPath = resolveDatabasePath(databasePath);
loadSymbols(resolvedPath / "symbols.json");
loadTree(resolvedPath / "tree.json");
}
return true;
}
catch (const std::exception &e)
{
m_lastError = e.what();
m_symbols.clear();
m_root.reset();
return false;
}
}
std::vector<SceSymbolMatch> scan(const std::vector<Section> &sections) const
{
std::unordered_map<uint32_t, std::map<std::string, Candidate>> candidatesByAddress;
if (!m_root)
{
return {};
}
for (const Section &section : sections)
{
if (!section.isCode || section.data == nullptr || section.size < 4)
{
continue;
}
for (uint32_t offset = 0; offset + 4 <= section.size; offset += 4)
{
const std::vector<const SymbolRecord *> symbols = findCandidateSymbols(section, offset);
if (symbols.empty())
{
continue;
}
for (const SymbolRecord *symbol : symbols)
{
if (symbol == nullptr || !symbol->isFunction || symbol->size == 0)
{
continue;
}
if (offset > section.size || symbol->size > section.size - offset)
{
continue;
}
if (!matchesSymbol(section, offset, *symbol))
{
continue;
}
uint32_t actualSize = symbol->size;
while (actualSize <= section.size - offset - 4 &&
readLe32(section.data + offset + actualSize) == 0)
{
actualSize += 4;
}
Candidate candidate;
candidate.symbol = symbol;
candidate.address = section.address + offset;
candidate.actualSize = actualSize;
candidatesByAddress[candidate.address][makeSymbolKey(*symbol)] = candidate;
}
}
}
return resolveCandidates(candidatesByAddress);
}
const std::string &lastError() const
{
return m_lastError;
}
private:
std::unordered_map<std::string, SymbolRecord> m_symbols;
std::unique_ptr<MatchNode> m_root;
std::string m_lastError;
void loadEmbeddedSymbols()
{
const std::string jsonText = joinJsonChunks(sce_symbol_database::kSymbolsJsonChunks);
loadSymbolsJson(nlohmann::json::parse(jsonText));
}
void loadEmbeddedTree()
{
const std::string jsonText = joinJsonChunks(sce_symbol_database::kTreeJsonChunks);
loadTreeJson(nlohmann::json::parse(jsonText));
}
void loadSymbols(const fs::path &path)
{
std::ifstream file(path);
if (!file)
{
throw std::runtime_error("unable to open " + path.string());
}
const nlohmann::json root = nlohmann::json::parse(file);
loadSymbolsJson(root);
}
void loadSymbolsJson(const nlohmann::json &root)
{
for (auto libraryIt = root.begin(); libraryIt != root.end(); ++libraryIt)
{
const std::string library = libraryIt.key();
for (auto nameIt = libraryIt.value().begin(); nameIt != libraryIt.value().end(); ++nameIt)
{
const std::string name = nameIt.key();
for (auto hashIt = nameIt.value().begin(); hashIt != nameIt.value().end(); ++hashIt)
{
const std::string hash = hashIt.key();
for (auto variantIt = hashIt.value().begin(); variantIt != hashIt.value().end(); ++variantIt)
{
SymbolRecord symbol;
symbol.library = library;
symbol.name = name;
symbol.hashText = hash;
symbol.hash = parseSha1(hash);
symbol.variantHash = static_cast<uint32_t>(std::stoul(variantIt.key(), nullptr, 16));
const nlohmann::json &jsonSymbol = variantIt.value();
symbol.size = jsonSymbol.value("size", 0u);
const std::string type = toUpperAscii(jsonSymbol.value("type", std::string()));
symbol.isFunction = (type == "FUNCTION" || type == "FUNC");
const nlohmann::json relocations =
jsonSymbol.value("relocations", nlohmann::json::object());
for (auto relocationIt = relocations.begin(); relocationIt != relocations.end(); ++relocationIt)
{
RelocationRecord relocation;
relocation.offset = static_cast<uint32_t>(std::stoul(relocationIt.key(), nullptr, 0));
relocation.type = parseRelocationType(relocationIt.value().value("type", std::string("none")));
symbol.relocations.push_back(relocation);
}
m_symbols[makeSymbolKey(symbol)] = std::move(symbol);
}
}
}
}
}
void loadTree(const fs::path &path)
{
std::ifstream file(path);
if (!file)
{
throw std::runtime_error("unable to open " + path.string());
}
const nlohmann::json root = nlohmann::json::parse(file);
loadTreeJson(root);
}
void loadTreeJson(const nlohmann::json &root)
{
m_root = parseNode(root);
}
std::unique_ptr<MatchNode> parseNode(const nlohmann::json &jsonNode) const
{
auto node = std::make_unique<MatchNode>();
node->offset = jsonNode.value("offset", 0u);
if (jsonNode.contains("symbols"))
{
for (const nlohmann::json &jsonSymbol : jsonNode["symbols"])
{
MatchSymbolKey symbol;
symbol.library = jsonSymbol.value("library", std::string());
symbol.name = jsonSymbol.value("name", std::string());
symbol.hash = jsonSymbol.value("hash", std::string());
symbol.variantHash = jsonSymbol.value("variant", 0u);
node->symbols.push_back(std::move(symbol));
}
}
if (jsonNode.contains("next"))
{
for (const nlohmann::json &jsonEdge : jsonNode["next"])
{
MatchEdge edge;
const nlohmann::json &match = jsonEdge["match"];
edge.value = match.value("value", 0u);
if (match.contains("relocation") && match["relocation"].contains("type"))
{
edge.relocationType = parseRelocationType(match["relocation"].value("type", std::string("none")));
}
edge.child = parseNode(jsonEdge["child"]);
node->next.push_back(std::move(edge));
}
}
return node;
}
const SymbolRecord *findSymbol(const MatchSymbolKey &key) const
{
const auto it = m_symbols.find(makeSymbolKey(key.library, key.name, key.hash, key.variantHash));
if (it == m_symbols.end())
{
return nullptr;
}
return &it->second;
}
std::vector<const SymbolRecord *> findCandidateSymbols(const Section &section, uint32_t offset) const
{
std::vector<const SymbolRecord *> symbols;
std::vector<const MatchNode *> stack;
stack.push_back(m_root.get());
while (!stack.empty())
{
const MatchNode *node = stack.back();
stack.pop_back();
if (node == nullptr || node->offset > section.size || offset > section.size - node->offset)
{
continue;
}
if (section.size - offset - node->offset < 4)
{
continue;
}
const uint32_t value = readLe32(section.data + offset + node->offset);
for (const MatchEdge &edge : node->next)
{
const uint32_t mask = relocationMask(edge.relocationType);
if ((value & mask) != (edge.value & mask))
{
continue;
}
for (const MatchSymbolKey &key : edge.child->symbols)
{
if (const SymbolRecord *symbol = findSymbol(key))
{
symbols.push_back(symbol);
}
}
if (!edge.child->next.empty())
{
stack.push_back(edge.child.get());
}
}
}
return symbols;
}
bool matchesSymbol(const Section &section, uint32_t offset, const SymbolRecord &symbol) const
{
std::vector<uint8_t> bytes(section.data + offset, section.data + offset + symbol.size);
for (const RelocationRecord &relocation : symbol.relocations)
{
if (relocation.offset > bytes.size() || bytes.size() - relocation.offset < 4)
{
continue;
}
const uint32_t value = readLe32(bytes.data() + relocation.offset);
writeLe32(bytes.data() + relocation.offset,
disabledRelocationValue(relocation.type, value));
}
return sha1(bytes) == symbol.hash;
}
std::vector<SceSymbolMatch> resolveCandidates(
const std::unordered_map<uint32_t, std::map<std::string, Candidate>> &candidatesByAddress) const
{
std::vector<SceSymbolMatch> matches;
matches.reserve(candidatesByAddress.size());
for (const auto &[address, candidatesByKey] : candidatesByAddress)
{
std::vector<const Candidate *> viable;
viable.reserve(candidatesByKey.size());
for (const auto &[_, candidate] : candidatesByKey)
{
if (candidate.symbol != nullptr && candidate.symbol->staticBitCount() >= 256)
{
viable.push_back(&candidate);
}
}
if (viable.empty())
{
continue;
}
// The upstream scanner also uses dependency and adjacent-library context.
// This analyzer integration keeps only unambiguous direct hash matches for now.
std::set<std::string> identities;
for (const Candidate *candidate : viable)
{
identities.insert(candidate->symbol->library + '\n' + candidate->symbol->name);
}
if (identities.size() != 1)
{
continue;
}
const Candidate *best = *std::max_element(
viable.begin(),
viable.end(),
[](const Candidate *lhs, const Candidate *rhs)
{
if (lhs->actualSize != rhs->actualSize)
{
return lhs->actualSize < rhs->actualSize;
}
return lhs->symbol->staticBitCount() < rhs->symbol->staticBitCount();
});
SceSymbolMatch match;
match.address = address;
match.size = best->actualSize;
match.name = best->symbol->name;
match.library = best->symbol->library;
match.hash = best->symbol->hashText;
match.variantHash = best->symbol->variantHash;
matches.push_back(std::move(match));
}
std::sort(matches.begin(), matches.end(),
[](const SceSymbolMatch &a, const SceSymbolMatch &b)
{
return a.address < b.address;
});
return matches;
}
};
SceSymbolScanner::SceSymbolScanner()
: m_impl(std::make_unique<Impl>())
{
}
SceSymbolScanner::~SceSymbolScanner() = default;
bool SceSymbolScanner::loadDatabase(const std::string &databasePath)
{
return m_impl->loadDatabase(databasePath);
}
std::vector<SceSymbolMatch> SceSymbolScanner::scan(const std::vector<Section> &sections) const
{
return m_impl->scan(sections);
}
const std::string &SceSymbolScanner::lastError() const
{
return m_impl->lastError();
}
}
+243
View File
@@ -0,0 +1,243 @@
#include "ps2recomp/toml_generator.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <vector>
namespace fs = std::filesystem;
namespace ps2recomp
{
bool TomlGenerator::generate(const TomlGeneratorInput &input, const std::string &outputPath)
{
std::ofstream file(outputPath);
if (!file)
{
std::cerr << "Failed to open output file: " << outputPath << std::endl;
return false;
}
fs::path elfPathObj(input.elfPath);
std::string elfFileName = elfPathObj.filename().string();
fs::path outputPathObj(outputPath);
fs::path outputDir = outputPathObj.parent_path();
if (outputDir.empty())
{
outputDir = ".";
}
const fs::path generatedOutputDir = outputDir / "output";
std::string outputDirStr = generatedOutputDir.generic_string() + "/";
if (!fs::exists(generatedOutputDir))
{
fs::create_directories(generatedOutputDir);
}
file << "# PS2Recomp configuration for: " << elfFileName << "\n";
file << "# Generated by ElfAnalyzer\n\n";
file << "[general]\n";
file << "# Path to input ELF file\n";
file << "input = \"" << escapeBackslashes(input.elfPath) << "\"\n\n";
file << "# Path to Ghidra exported function map (optional CSV)\n";
file << "ghidra_output = \"\"\n\n";
file << "# Path to output directory\n";
file << "output = \"" << escapeBackslashes(outputDirStr) << "\"\n\n";
file << "# Single file output mode (recommended for large games)\n";
file << "single_file_output = false\n\n";
file << "# Patch policy (instruction-driven handling is preferred for syscalls)\n";
file << "patch_syscalls = false\n";
file << "patch_cop0 = true\n";
file << "patch_cache = true\n\n";
std::unordered_map<std::string, size_t> functionNameCounts;
functionNameCounts.reserve(input.context.functions.size());
for (const auto &func : input.context.functions)
{
if (!func.name.empty())
{
functionNameCounts[func.name]++;
}
}
auto makeSelector = [&](const std::string &name, uint32_t start) -> std::string
{
auto it = functionNameCounts.find(name);
if (it == functionNameCounts.end() || it->second <= 1)
{
return name;
}
std::stringstream selector;
selector << name << "@0x"
<< std::hex << std::uppercase << std::setw(8) << std::setfill('0')
<< start;
return selector.str();
};
auto collectFunctionSelectors =
[&](const std::unordered_set<std::string> &nameSet) -> std::vector<std::string>
{
std::vector<const Function *> orderedFunctions;
orderedFunctions.reserve(input.context.functions.size());
for (const auto &func : input.context.functions)
{
orderedFunctions.push_back(&func);
}
std::sort(orderedFunctions.begin(), orderedFunctions.end(),
[](const Function *a, const Function *b)
{ return a->start < b->start; });
std::vector<std::string> entries;
std::unordered_set<std::string> seenEntries;
std::unordered_set<std::string> coveredNames;
for (const Function *func : orderedFunctions)
{
if (!nameSet.contains(func->name))
{
continue;
}
coveredNames.insert(func->name);
const std::string entry = makeSelector(func->name, func->start);
if (seenEntries.insert(entry).second)
{
entries.push_back(entry);
}
}
std::vector<std::string> leftovers;
leftovers.reserve(nameSet.size());
for (const auto &name : nameSet)
{
if (!coveredNames.contains(name) && seenEntries.insert(name).second)
{
leftovers.push_back(name);
}
}
std::sort(leftovers.begin(), leftovers.end());
entries.insert(entries.end(), leftovers.begin(), leftovers.end());
return entries;
};
const std::vector<std::string> stubEntries = collectFunctionSelectors(input.libFunctions);
const std::vector<std::string> untrackedStubEntries = collectFunctionSelectors(input.untrackedStubFunctions);
file << "# Functions to stub (only names with runtime syscall/stub handlers)\n";
file << "stubs = [\n";
for (const auto &func : stubEntries)
{
file << " \"" << func << "\",\n";
}
file << "]\n\n";
file << "# Detected library-like functions without runtime handlers.\n";
file << "# This is informational only; PS2Recomp ignores this list and recompiles them normally.\n";
file << "untracked_stubs = [\n";
for (const auto &func : untrackedStubEntries)
{
file << " \"" << func << "\",\n";
}
file << "]\n\n";
file << "# Legacy compatibility field. The analyzer no longer auto-populates skip entries.\n";
file << "skip = []\n\n";
if (!input.mmioByInstructionAddress.empty())
{
file << "# Detected MMIO accesses\n";
file << "[mmio]\n";
for (const auto &[instAddr, mmioAddr] : input.mmioByInstructionAddress)
{
file << "\"0x" << std::hex << instAddr << "\" = \"0x" << mmioAddr << "\"\n"
<< std::dec;
}
file << "\n";
}
if (!input.jumpTables.empty())
{
file << "# Jump tables detected in the program\n";
file << "[jump_tables]\n";
for (const auto &jt : input.jumpTables)
{
file << "[[jump_tables.table]]\n";
file << "address = \"0x" << std::hex << jt.address << "\"\n"
<< std::dec;
file << "entries = [\n";
for (const auto &[index, target] : jt.entries)
{
file << " { index = " << index << ", target = \"0x"
<< std::hex << target << "\" },\n"
<< std::dec;
}
file << "]\n\n";
}
}
if (!input.patches.empty())
{
file << "# Patches to apply during recompilation\n";
file << "[patches]\n";
file << "# Individual instruction patches\n";
file << "instructions = [\n";
for (const auto &[address, value] : input.patches)
{
auto reasonIt = input.patchReasons.find(address);
const std::string reason = reasonIt == input.patchReasons.end() ? "" : reasonIt->second;
file << " { address = \"0x" << std::hex << address << "\", value = \"0x"
<< std::hex << value << "\" }, # " << reason << "\n";
}
file << "]\n\n";
}
file << "# Performance critical functions (may need manual optimization)\n";
file << "[performance]\n";
file << "critical = [\n";
for (const auto &func : input.context.functions)
{
auto reasonIt = input.performanceCriticalReasons.find(func.start);
if (reasonIt != input.performanceCriticalReasons.end())
{
file << " \"" << func.name << "\", # " << reasonIt->second << "\n";
}
}
file << "]\n\n";
std::cout << "Generated TOML configuration: " << outputPath << std::endl;
return true;
}
std::string TomlGenerator::escapeBackslashes(const std::string &path)
{
std::string result;
for (char ch : path)
{
if (ch == '\\')
{
result.append("\\\\");
}
else
{
result.push_back(ch);
}
}
return result;
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ include(FetchContent)
FetchContent_Declare(
elfio
GIT_REPOSITORY https://github.com/serge1/ELFIO.git
GIT_TAG 7d30a22fc5aac06adfe7887ae57f3701b6b5f913
GIT_TAG Release_3.12
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(elfio)
-66
View File
@@ -1,66 +0,0 @@
[general]
# Path to input ELF file
input = "path/to/your/ps2_game.elf"
# Path to output directory
output = "output/"
# Single file output mode (false for one file per function)
single_file_output = false
# Lower peak memory by avoiding retained disassembly strings and forcing serial output generation.
low_memory_mode = false
# Function generation workers. 0 uses nproc - 1 when at least 2 hardware threads are available; 1 disables parallel generation.
# Limited to nproc * 2 to avoid oversubscription.
output_worker_threads = 0
# Path to runtime header (optional)
runtime_header = "include/ps2_runtime.h"
# Functions to stub (these will generate wrappers to runtime syscall/stub handlers when names match)
# You can also bind stripped functions by address with "handler@0xADDRESS".
# Generic temporary handlers are available: ret0, ret1, reta0.
stubs = [
"printf",
"malloc",
"free",
"memcpy",
"memset",
"strncpy",
"sprintf",
# "sceCdRead@0x00123456",
# "SifLoadModule@0x00127890",
# "ret0@0x001D9410",
]
# Functions to skip (these will not be recompiled)
skip = ["abort", "exit", "_exit"]
# Patches to apply during recompilation
[patches]
# Individual instruction patches
instructions = [
{ address = "0x100004", value = "0x00000000" }, # NOP an instruction
{ address = "0x100104", value = "0x24040000" }, # Change an immediate value
]
# Function hook patches (not yet implemented)
[[patches.hook]]
function = "printf"
code = '''
// Custom printf implementation
void printf(uint8_t* rdram, R5900Context* ctx) {
// Implementation here
}
'''
# Function replacement patches (not yet implemented)
[[patches.func]]
address = "0x100000"
code = '''
// Custom implementation for function at 0x100000
void func_00100000(uint8_t* rdram, R5900Context* ctx) {
// Implementation here
}
'''
+195 -120
View File
@@ -32,33 +32,128 @@ import java.util.regex.Pattern;
public class ExportPS2Functions extends GhidraScript {
private static final Set<String> SYSTEM_FUNCTION_NAMES = new HashSet<>(Arrays.asList(
"entry", "_start", "_init", "_fini",
"abort", "exit", "_exit",
"_profiler_start", "_profiler_stop",
"__main", "__do_global_ctors", "__do_global_dtors",
"_GLOBAL__sub_I_", "_GLOBAL__sub_D_",
"__ctor_list", "__dtor_list", "_edata", "_end",
"etext", "__exidx_start", "__exidx_end",
"_ftext", "__bss_start", "__bss_start__",
"__bss_end__", "__end__", "_stack", "_dso_handle"
));
private static final Set<String> DO_NOT_SKIP_OR_STUB = new HashSet<>(Arrays.asList(
"entry",
"_start",
"_init",
"topThread",
"cmd_sem_init"
));
private static final Set<String> KNOWN_LOCAL_HELPER_NAMES = new HashSet<>(Arrays.asList(
"memcpy2",
"_memcpy2"
// For now I have to copy all functions from the runtime handler list
private static final Set<String> RUNTIME_HANDLER_NAMES = new HashSet<>(Arrays.asList(
"FlushCache", "iFlushCache", "ResetEE", "SetMemoryMode", "InitThread", "CreateThread",
"DeleteThread", "StartThread", "ExitThread", "ExitDeleteThread", "TerminateThread", "SuspendThread",
"ResumeThread", "GetThreadId", "ReferThreadStatus", "iReferThreadStatus", "SleepThread", "WakeupThread",
"iWakeupThread", "CancelWakeupThread", "iCancelWakeupThread", "ChangeThreadPriority", "iChangeThreadPriority", "RotateThreadReadyQueue",
"iRotateThreadReadyQueue", "ReleaseWaitThread", "iReleaseWaitThread", "CreateSema", "DeleteSema", "SignalSema",
"iSignalSema", "WaitSema", "PollSema", "iPollSema", "ReferSemaStatus", "iReferSemaStatus",
"CreateEventFlag", "DeleteEventFlag", "SetEventFlag", "iSetEventFlag", "ClearEventFlag", "iClearEventFlag",
"WaitEventFlag", "PollEventFlag", "iPollEventFlag", "ReferEventFlagStatus", "iReferEventFlagStatus", "InitAlarm",
"SetAlarm", "iSetAlarm", "CancelAlarm", "iCancelAlarm", "ReleaseAlarm", "iReleaseAlarm",
"AddIntcHandler", "AddIntcHandler2", "RemoveIntcHandler", "AddDmacHandler", "AddDmacHandler2", "RemoveDmacHandler",
"EnableIntc", "iEnableIntc", "DisableIntc", "iDisableIntc", "EnableDmac", "iEnableDmac",
"DisableDmac", "iDisableDmac", "SifStopModule", "SifLoadModule", "SifInitRpc", "SifBindRpc",
"SifCallRpc", "SifRegisterRpc", "SifCheckStatRpc", "SifSetRpcQueue", "SifRemoveRpcQueue", "SifRemoveRpc",
"sceSifCallRpc", "sceSifSendCmd", "sceRpcGetPacket", "fioOpen", "fioClose", "fioRead",
"fioWrite", "fioLseek", "fioMkdir", "fioChdir", "fioRmdir", "fioGetstat",
"fioRemove", "SetGsCrt", "GsSetCrt", "GsGetIMR", "iGsGetIMR", "GsPutIMR",
"iGsPutIMR", "SetVSyncFlag", "SetSyscall", "GsSetVideoMode", "GetOsdConfigParam", "SetOsdConfigParam",
"EnableCache", "DisableCache", "GetRomName", "SifLoadElfPart", "sceSifLoadElf", "sceSifLoadElfPart",
"sceSifLoadModule", "sceSifLoadModuleBuffer", "SetupThread", "EndOfHeap", "GetMemorySize", "Deci2Call",
"QueryBootMode", "GetThreadTLS", "RegisterExitHandler", "ret0", "ret1", "reta0",
"calloc_r", "free_r", "malloc_r", "malloc_trim_r", "mbtowc_r", "printf_r",
"abs", "__ieee754_rem_pio2f", "__kernel_cosf", "__kernel_sinf", "atan", "atan2",
"calloc", "ceil", "close", "cos", "exit", "exp",
"fabs", "fclose", "fflush", "floor", "fopen", "fprintf",
"fread", "free", "fseek", "fstat", "ftell", "fwrite",
"getpid", "log", "log10", "lseek", "malloc", "memchr",
"memcmp", "memcpy", "memmove", "memset", "open", "pow",
"printf", "puts", "rand", "read", "realloc", "sin",
"snprintf", "sprintf", "sqrt", "srand", "stat", "strcasecmp",
"strcat", "strchr", "strcmp", "strcpy", "strlen", "strncat",
"strncmp", "strncpy", "strrchr", "strstr", "tan", "vfprintf",
"vsprintf", "write", "DmaAddr", "builtin_set_imask", "sceCdRI", "sceCdRM",
"sceDevVif0Reset", "sceDevVu0Reset", "sceFsDbChk", "sceFsIntrSigSema", "sceFsSemExit", "sceFsSemInit",
"sceFsSigSema", "sceIDC", "sceMpegFlush", "sceRpcFreePacket", "sceRpcGetFPacket", "sceRpcGetFPacket2",
"sceSDC", "sceSifCmdIntrHdlr", "sceVu0ecossin", "mcCallMessageTypeSe", "mcCheckReadStartConfigFile", "mcCheckReadStartSaveFile",
"mcCheckWriteStartConfigFile", "mcCheckWriteStartSaveFile", "mcCreateConfigInit", "mcCreateFileSelectWindow", "mcCreateIconInit", "mcCreateSaveFileInit",
"mcDispFileName", "mcDispFileNumber", "mcDispWindowCurSol", "mcDispWindowFoundtion", "mcDisplayFileSelectWindow", "mcDisplaySelectFileInfo",
"mcDisplaySelectFileInfoMesCount", "mcGetConfigCapacitySize", "mcGetFileSelectWindowCursol", "mcGetFreeCapacitySize", "mcGetIconCapacitySize", "mcGetIconFileCapacitySize",
"mcGetPortSelectDirInfo", "mcGetSaveFileCapacitySize", "mcGetStringEnd", "mcMoveFileSelectWindowCursor", "mcNewCreateConfigFile", "mcNewCreateIcon",
"mcNewCreateSaveFile", "mcReadIconData", "mcReadStartConfigFile", "mcReadStartSaveFile", "mcSelectFileInfoInit", "mcSelectSaveFileCheck",
"mcSetFileSelectWindowCursol", "mcSetFileSelectWindowCursolInit", "mcSetStringSaveFile", "mcSetTyepWriteMode", "mcWriteIconData", "mcWriteStartConfigFile",
"mcWriteStartSaveFile", "mceGetInfoApdx", "mceIntrReadFixAlign", "mceStorePwd", "sceCdApplyNCmd", "sceCdBreak",
"sceCdCallback", "sceCdChangeThreadPriority", "sceCdDelayThread", "sceCdDiskReady", "sceCdGetDiskType", "sceCdGetError",
"sceCdGetReadPos", "sceCdGetToc", "sceCdInit", "sceCdInitEeCB", "sceCdIntToPos", "sceCdMmode",
"sceCdNcmdDiskReady", "sceCdPause", "sceCdPosToInt", "sceCdRead", "sceCdReadChain", "sceCdReadClock",
"sceCdReadIOPm", "sceCdSearchFile", "sceCdSeek", "sceCdStInit", "sceCdStPause", "sceCdStRead",
"sceCdStResume", "sceCdStSeek", "sceCdStSeekF", "sceCdStStart", "sceCdStStat", "sceCdStStop",
"sceCdStandby", "sceCdStatus", "sceCdStop", "sceCdStream", "sceCdSync", "sceCdSyncS",
"sceCdTrayReq", "sceClose", "sceDeci2Close", "sceDeci2ExLock", "sceDeci2ExRecv", "sceDeci2ExReqSend",
"sceDeci2ExSend", "sceDeci2ExUnLock", "sceDeci2Open", "sceDeci2Poll", "sceDeci2ReqSend", "sceDmaCallback",
"sceDmaDebug", "sceDmaGetChan", "sceDmaGetEnv", "sceDmaLastSyncTime", "sceDmaPause", "sceDmaPutEnv",
"sceDmaPutStallAddr", "sceDmaRecv", "sceDmaRecvI", "sceDmaRecvN", "sceDmaReset", "sceDmaRestart",
"sceDmaSend", "sceDmaSendI", "sceDmaSendM", "sceDmaSendN", "sceDmaSync", "sceDmaSyncN",
"sceDmaWatch", "sceFsInit", "sceFsReset", "sceGifPkAddGsAD", "sceGifPkAddGsData", "sceGifPkCloseGifTag",
"sceGifPkCnt", "sceGifPkEnd", "sceGifPkInit", "sceGifPkOpenGifTag", "sceGifPkRef", "sceGifPkRefLoadImage",
"sceGifPkReset", "sceGifPkReserve", "sceGifPkTerminate", "sceGsExecLoadImage", "sceGsExecStoreImage", "sceGsGetGParam",
"sceGsPutDispEnv", "sceGsPutDrawEnv", "sceGsResetGraph", "sceGsResetPath", "sceGsSetDefClear", "sceGsSetDefDBuffDc",
"sceGsSetDefDBuff", "sceGsSetDefDispEnv", "sceGsSetDefDrawEnv", "sceGsSetDefDrawEnv2", "sceGsSetDefLoadImage", "sceGsSetDefStoreImage",
"sceGsSwapDBuffDc", "sceGsSwapDBuff", "sceGsSyncPath", "sceGsSyncV", "sceGsSyncVCallback", "sceGszbufaddr",
"sceVif1PkAddGsAD", "sceVif1PkAlign", "sceVif1PkCall", "sceVif1PkCloseDirectCode", "sceVif1PkCloseGifTag", "sceVif1PkCnt",
"sceVif1PkEnd", "sceVif1PkInit", "sceVif1PkOpenDirectCode", "sceVif1PkOpenGifTag", "sceVif1PkReset", "sceVif1PkReserve",
"sceVif1PkTerminate", "sceeFontInit", "sceeFontLoadFont", "sceeFontPrintfAt", "sceeFontPrintfAt2", "sceeFontGenerateString",
"sceeFontClose", "sceeFontSetColour", "sceeFontSetMode", "sceeFontSetFont", "sceeFontSetScale", "sceIoctl",
"sceIpuInit", "sceIpuRestartDMA", "sceIpuStopDMA", "sceIpuSync", "sceLseek", "sceMcChangeThreadPriority",
"sceMcChdir", "sceMcClose", "sceMcDelete", "sceMcEnd", "sceMcFlush", "sceMcFormat",
"sceMcGetDir", "sceMcGetEntSpace", "sceMcGetInfo", "sceMcGetSlotMax", "sceMcInit", "sceMcMkdir",
"sceMcOpen", "sceMcRead", "sceMcRename", "sceMcSeek", "sceMcSetFileInfo", "sceMcSync",
"sceMcUnformat", "sceMcWrite", "sceMpegAddBs", "sceMpegAddCallback", "sceMpegAddStrCallback", "sceMpegClearRefBuff",
"sceMpegCreate", "sceMpegDelete", "sceMpegDemuxPss", "sceMpegDemuxPssRing", "sceMpegDispCenterOffX", "sceMpegDispCenterOffY",
"sceMpegDispHeight", "sceMpegDispWidth", "sceMpegGetDecodeMode", "sceMpegGetPicture", "sceMpegGetPictureRAW8", "sceMpegGetPictureRAW8xy",
"sceMpegInit", "sceMpegIsEnd", "sceMpegIsRefBuffEmpty", "sceMpegReset", "sceMpegResetDefaultPtsGap", "sceMpegSetDecodeMode",
"sceMpegSetDefaultPtsGap", "sceMpegSetImageBuff", "sceOpen", "scePadEnd", "scePadEnterPressMode", "scePadExitPressMode",
"scePadGetButtonMask", "scePadGetDmaStr", "scePadGetFrameCount", "scePadGetModVersion", "scePadGetPortMax", "scePadGetReqState",
"scePadGetSlotMax", "scePadGetState", "scePadInfoAct", "scePadInfoComb", "scePadInfoMode", "scePadInfoPressMode",
"scePadInit", "scePadInit2", "scePadPortClose", "scePadPortOpen", "scePadRead", "scePadReqIntToStr",
"scePadSetActAlign", "scePadSetActDirect", "scePadSetButtonInfo", "scePadSetMainMode", "scePadSetReqState", "scePadSetVrefParam",
"scePadSetWarningLevel", "scePadStateIntToStr", "scePrintf", "sceRead", "sceResetttyinit", "sceSSyn_BreakAtick",
"sceSSyn_ClearBreakAtick", "sceSSyn_SendExcMsg", "sceSSyn_SendNrpnMsg", "sceSSyn_SendRpnMsg", "sceSSyn_SendShortMsg", "sceSSyn_SetChPriority",
"sceSSyn_SetMasterVolume", "sceSSyn_SetOutPortVolume", "sceSSyn_SetOutputAssign", "sceSSyn_SetOutputMode", "sceSSyn_SetPortMaxPoly", "sceSSyn_SetPortVolume",
"sceSSyn_SetTvaEnvMode", "sceSdCallBack", "sceSdRemote", "sceSdRemoteInit", "sceSdTransToIOP", "sceSetBrokenLink",
"sceSetPtm", "sceSifAddCmdHandler", "sceSifAllocIopHeap", "sceSifAllocSysMemory", "sceSifBindRpc", "sceSifCheckStatRpc",
"sceSifDmaStat", "sceSifExecRequest", "sceSifExitCmd", "sceSifExitRpc", "sceSifFreeIopHeap", "sceSifFreeSysMemory",
"sceSifGetDataTable", "sceSifGetIopAddr", "sceSifGetNextRequest", "sceSifGetOtherData", "sceSifGetReg", "sceSifGetSreg",
"sceSifInitCmd", "sceSifInitIopHeap", "sceSifInitRpc", "sceSifIsAliveIop", "sceSifLoadFileReset", "sceSifLoadIopHeap",
"sceSifRebootIop", "sceSifRegisterRpc", "sceSifRemoveCmdHandler", "sceSifRemoveRpc", "sceSifRemoveRpcQueue", "sceSifResetIop",
"sceSifRpcLoop", "sceSifSetCmdBuffer", "sceSifSetDChain", "sceSifSetDma", "isceSifSetDChain", "isceSifSetDma",
"sceSifSetIopAddr", "sceSifSetReg", "sceSifSetRpcQueue", "sceSifSetSreg", "sceSifSetSysCmdBuffer", "sceSifStopDma",
"sceSifSyncIop", "sceSifWriteBackDCache", "sceSynthSizerLfoTriangle", "sceSynthesizerAmpProcI", "sceSynthesizerAmpProcNI", "sceSynthesizerAssignAllNoteOff",
"sceSynthesizerAssignAllSoundOff", "sceSynthesizerAssignHoldChange", "sceSynthesizerAssignNoteOff", "sceSynthesizerAssignNoteOn", "sceSynthesizerCalcEnv", "sceSynthesizerCalcPortamentPitch",
"sceSynthesizerCalcTvfCoefAll", "sceSynthesizerCalcTvfCoefF0", "sceSynthesizerCent2PhaseInc", "sceSynthesizerChangeEffectSend", "sceSynthesizerChangeHsPanpot", "sceSynthesizerChangeNrpnCutOff",
"sceSynthesizerChangeNrpnLfoDepth", "sceSynthesizerChangeNrpnLfoRate", "sceSynthesizerChangeOutAttrib", "sceSynthesizerChangeOutVol", "sceSynthesizerChangePanpot", "sceSynthesizerChangePartBendSens",
"sceSynthesizerChangePartExpression", "sceSynthesizerChangePartHsExpression", "sceSynthesizerChangePartHsPitchBend", "sceSynthesizerChangePartModuration", "sceSynthesizerChangePartPitchBend", "sceSynthesizerChangePartVolume",
"sceSynthesizerChangePortamento", "sceSynthesizerChangePortamentoTime", "sceSynthesizerClearKeyMap", "sceSynthesizerClearSpr", "sceSynthesizerCopyOutput", "sceSynthesizerDmaFromSPR",
"sceSynthesizerDmaSpr", "sceSynthesizerDmaToSPR", "sceSynthesizerGetPartOutLevel", "sceSynthesizerGetPartial", "sceSynthesizerGetSampleParam", "sceSynthesizerHsMessage",
"sceSynthesizerLfoNone", "sceSynthesizerLfoProc", "sceSynthesizerLfoSawDown", "sceSynthesizerLfoSawUp", "sceSynthesizerLfoSquare", "sceSynthesizerReadNoise",
"sceSynthesizerReadNoiseAdd", "sceSynthesizerReadSample16", "sceSynthesizerReadSample16Add", "sceSynthesizerReadSample8", "sceSynthesizerReadSample8Add", "sceSynthesizerResetPart",
"sceSynthesizerRestorDma", "sceSynthesizerSelectPatch", "sceSynthesizerSendShortMessage", "sceSynthesizerSetMasterVolume", "sceSynthesizerSetRVoice", "sceSynthesizerSetupDma",
"sceSynthesizerSetupLfo", "sceSynthesizerSetupMidiModuration", "sceSynthesizerSetupMidiPanpot", "sceSynthesizerSetupNewNoise", "sceSynthesizerSetupReleaseEnv", "sceSynthesizerSetupTruncateTvaEnv",
"sceSynthesizerSetupTruncateTvfPitchEnv", "sceSynthesizerSetuptEnv", "sceSynthesizerTonegenerator", "sceSynthesizerTransposeMatrix", "sceSynthesizerTvfProcI", "sceSynthesizerTvfProcNI",
"sceSynthesizerWaitDmaFromSPR", "sceSynthesizerWaitDmaToSPR", "sceSynthsizerGetDrumPatch", "sceSynthsizerGetMeloPatch", "sceSynthsizerLfoNoise", "sceTtyHandler",
"sceTtyInit", "sceTtyRead", "sceTtyWrite", "sceVpu0Reset", "sceVu0AddVector", "sceVu0ApplyMatrix",
"sceVu0CameraMatrix", "sceVu0ClampVector", "sceVu0ClipAll", "sceVu0ClipScreen", "sceVu0ClipScreen3", "sceVu0CopyMatrix",
"sceVu0CopyVector", "sceVu0CopyVectorXYZ", "sceVu0DivVector", "sceVu0DivVectorXYZ", "sceVu0DropShadowMatrix", "sceVu0FTOI0Vector",
"sceVu0FTOI4Vector", "sceVu0ITOF0Vector", "sceVu0ITOF12Vector", "sceVu0ITOF4Vector", "sceVu0InnerProduct", "sceVu0InterVector",
"sceVu0InterVectorXYZ", "sceVu0InversMatrix", "sceVu0LightColorMatrix", "sceVu0MulMatrix", "sceVu0MulVector", "sceVu0NormalLightMatrix",
"sceVu0Normalize", "sceVu0OuterProduct", "sceVu0RotMatrix", "sceVu0RotMatrixX", "sceVu0RotMatrixY", "sceVu0RotMatrixZ",
"sceVu0RotTransPers", "sceVu0RotTransPersN", "sceVu0ScaleVector", "sceVu0ScaleVectorXYZ", "sceVu0SubVector", "sceVu0TransMatrix",
"sceVu0TransposeMatrix", "sceVu0UnitMatrix", "sceVu0ViewScreenMatrix", "sceWrite"
));
private static final Set<String> PS2_API_PREFIXES = new HashSet<>(Arrays.asList(
"sce", "sif", "gs", "dma", "iop", "vif", "spu", "mc", "libc"
"sce", "Sce", "SCE",
"sif", "Sif", "SIF",
"gs", "Gs", "GS",
"dma", "Dma", "DMA",
"iop", "Iop", "IOP",
"vif", "Vif", "VIF",
"spu", "Spu", "SPU",
"mc", "Mc", "MC",
"libc", "Libc", "LIBC"
));
private static final Set<String> KNOWN_STDLIB_NAMES = new HashSet<>(Arrays.asList(
@@ -112,7 +207,7 @@ public class ExportPS2Functions extends GhidraScript {
private enum ClassificationKind {
STUB,
SKIP,
UNTRACKED_STUB,
NONE
}
@@ -161,6 +256,32 @@ public class ExportPS2Functions extends GhidraScript {
return value.startsWith("_") && value.length() > 1 ? value.substring(1) : value;
}
private static String resolveRuntimeHandlerName(String name) {
if (name == null || name.isEmpty()) {
return "";
}
if (RUNTIME_HANDLER_NAMES.contains(name)) {
return name;
}
String normalized = normalizeOptionalLeadingUnderscore(name);
if (!normalized.equals(name) && RUNTIME_HANDLER_NAMES.contains(normalized)) {
return normalized;
}
String underscored = "_" + name;
if (!name.startsWith("_") && RUNTIME_HANDLER_NAMES.contains(underscored)) {
return underscored;
}
return "";
}
private static boolean hasRuntimeHandler(String name) {
return !resolveRuntimeHandlerName(name).isEmpty();
}
private static boolean hasReliableSymbolName(String name) {
if (name == null || name.isEmpty()) {
return false;
@@ -199,23 +320,24 @@ public class ExportPS2Functions extends GhidraScript {
return false;
}
String base = normalizeOptionalLeadingUnderscore(name).toLowerCase();
String base = normalizeOptionalLeadingUnderscore(name);
for (String prefix : PS2_API_PREFIXES) {
if (base.startsWith(prefix)) {
if (!base.startsWith(prefix)) {
continue;
}
if (base.length() == prefix.length()) {
return true;
}
if (!Character.isLowerCase(base.charAt(prefix.length()))) {
return true;
}
}
return false;
}
private static boolean isSystemSymbolNameForHeuristics(String name) {
if (!hasReliableSymbolName(name)) {
return false;
}
return SYSTEM_FUNCTION_NAMES.contains(name) || name.startsWith("__") || name.startsWith(".");
}
private static boolean matchesWithOptionalLeadingUnderscoreAlias(String candidate, Set<String> names) {
if (candidate == null || candidate.isEmpty() || names == null || names.isEmpty()) {
return false;
@@ -242,14 +364,15 @@ public class ExportPS2Functions extends GhidraScript {
return false;
}
if (KNOWN_LOCAL_HELPER_NAMES.contains(name)) {
return false;
if (hasRuntimeHandler(name)) {
return true;
}
String normalized = normalizeOptionalLeadingUnderscore(name);
if (KNOWN_LOCAL_HELPER_NAMES.contains(normalized)) {
return false;
if (hasRuntimeHandler(normalized)) {
return true;
}
if (KERNEL_RUNTIME_NAME_PATTERN.matcher(normalized).matches()) {
return true;
}
@@ -258,7 +381,7 @@ public class ExportPS2Functions extends GhidraScript {
return true;
}
if (hasPs2ApiPrefix(normalized)) {
if (hasPs2ApiPrefix(name)) {
return true;
}
@@ -271,36 +394,32 @@ public class ExportPS2Functions extends GhidraScript {
}
String name = function.getName();
if (name == null || name.isEmpty() || DO_NOT_SKIP_OR_STUB.contains(name)) {
return new ClassificationResult(ClassificationKind.NONE, name == null ? "" : name);
if (name == null || name.isEmpty()) {
return new ClassificationResult(ClassificationKind.NONE, "");
}
String runtimeName = resolveRuntimeHandlerName(name);
if (!runtimeName.isEmpty()) {
return new ClassificationResult(ClassificationKind.STUB, runtimeName);
}
if (function.isThunk()) {
if (isLibraryFunctionName(name)) {
return new ClassificationResult(ClassificationKind.STUB, name);
}
Function target = function.getThunkedFunction(true);
if (target != null) {
String targetName = target.getName();
String targetRuntimeName = resolveRuntimeHandlerName(targetName);
if (!targetRuntimeName.isEmpty()) {
return new ClassificationResult(ClassificationKind.STUB, targetRuntimeName);
}
if (isLibraryFunctionName(targetName)) {
return new ClassificationResult(ClassificationKind.STUB, targetName);
return new ClassificationResult(ClassificationKind.UNTRACKED_STUB, targetName);
}
}
if (isSystemSymbolNameForHeuristics(name)) {
return new ClassificationResult(ClassificationKind.SKIP, name);
}
return new ClassificationResult(ClassificationKind.NONE, name);
}
if (isLibraryFunctionName(name)) {
return new ClassificationResult(ClassificationKind.STUB, name);
}
if (isSystemSymbolNameForHeuristics(name)) {
return new ClassificationResult(ClassificationKind.SKIP, name);
return new ClassificationResult(ClassificationKind.UNTRACKED_STUB, name);
}
return new ClassificationResult(ClassificationKind.NONE, name);
@@ -313,48 +432,6 @@ public class ExportPS2Functions extends GhidraScript {
return name;
}
private static List<String> collectFunctionSelectors(
Set<String> names,
List<FunctionRecord> records,
boolean includeAddress
) {
List<FunctionRecord> ordered = new ArrayList<>(records);
ordered.sort(Comparator.comparingLong(r -> r.start));
List<String> selectors = new ArrayList<>();
Set<String> seenSelectors = new LinkedHashSet<>();
Set<String> coveredNames = new HashSet<>();
for (FunctionRecord record : ordered) {
if (record.name == null || !names.contains(record.name)) {
continue;
}
coveredNames.add(record.name);
String selector = makeSelector(record.name, record.start, includeAddress);
if (seenSelectors.add(selector)) {
selectors.add(selector);
}
}
if (includeAddress) {
List<String> unresolved = new ArrayList<>();
for (String name : names) {
if (!coveredNames.contains(name)) {
unresolved.add(name);
}
}
Collections.sort(unresolved);
for (String name : unresolved) {
System.out.println("Warning: unresolved selector name without address, omitting from TOML: " + name);
}
} else {
Collections.sort(selectors);
}
return selectors;
}
private boolean isExecutableAddress(Address address) {
if (address == null) {
return false;
@@ -534,9 +611,7 @@ public class ExportPS2Functions extends GhidraScript {
return;
}
boolean exportCsv = askYesNo("Export CSV", "Also export compatibility CSV function map?");
File csvFile = null;
csvFile = askFile("Choose output CSV file", "Save");
File csvFile = askFile("Choose output CSV file", "Save");
if (csvFile == null) {
return;
}
@@ -545,8 +620,8 @@ public class ExportPS2Functions extends GhidraScript {
FunctionIterator it = fm.getFunctions(true);
List<FunctionRecord> functionRecords = new ArrayList<>();
Set<String> stubNames = new LinkedHashSet<>();
Set<String> skipNames = new LinkedHashSet<>();
Set<String> stubSelectors = new LinkedHashSet<>();
Set<String> untrackedStubSelectors = new LinkedHashSet<>();
int uncategorizedCount = 0;
while (it.hasNext() && !monitor.isCancelled()) {
@@ -566,9 +641,9 @@ public class ExportPS2Functions extends GhidraScript {
ClassificationResult classification = classifyFunction(func);
if (classification.kind == ClassificationKind.STUB) {
stubNames.add(classification.name);
} else if (classification.kind == ClassificationKind.SKIP) {
skipNames.add(classification.name);
stubSelectors.add(makeSelector(classification.name, record.start, true));
} else if (classification.kind == ClassificationKind.UNTRACKED_STUB) {
untrackedStubSelectors.add(makeSelector(classification.name, record.start, true));
} else {
uncategorizedCount++;
}
@@ -580,9 +655,6 @@ public class ExportPS2Functions extends GhidraScript {
exportRecords.addAll(labelRecords);
exportRecords.sort(Comparator.comparingLong(r -> r.start));
List<String> stubSelectors = collectFunctionSelectors(stubNames, exportRecords, true);
List<String> skipSelectors = collectFunctionSelectors(skipNames, exportRecords, true);
try (PrintWriter writer = new PrintWriter(csvFile)) {
writer.println("Name,Start,End,Size");
for (FunctionRecord record : exportRecords) {
@@ -608,9 +680,10 @@ public class ExportPS2Functions extends GhidraScript {
writer.println("# Auto-generated by ExportPS2Functions.java");
writer.println("#");
writer.println("# Classification policy (aligned with analyzer intent):");
writer.println("# - library/runtime names -> [general].stubs");
writer.println("# - system names -> [general].skip");
writer.println("# - others are left for recompilation");
writer.println("# - runtime-known names -> [general].stubs");
writer.println("# - library-like names without runtime handlers -> [general].untracked_stubs");
writer.println("# - [general].skip is retained empty for legacy compatibility");
writer.println("# - no SCE symbol database is used by this Ghidra script");
writer.println();
writer.println("[general]");
@@ -626,11 +699,12 @@ public class ExportPS2Functions extends GhidraScript {
writer.println(" " + tomlString(selector) + ",");
}
writer.println("]");
writer.println("skip = [");
for (String selector : skipSelectors) {
writer.println("untracked_stubs = [");
for (String selector : untrackedStubSelectors) {
writer.println(" " + tomlString(selector) + ",");
}
writer.println("]");
writer.println("skip = []");
writer.println();
writer.println("[ghidra_export]");
@@ -638,15 +712,16 @@ public class ExportPS2Functions extends GhidraScript {
writer.println("code_label_count = " + labelRecords.size());
writer.println("csv_record_count = " + exportRecords.size());
writer.println("stub_count = " + stubSelectors.size());
writer.println("skip_count = " + skipSelectors.size());
writer.println("untracked_stub_count = " + untrackedStubSelectors.size());
writer.println("skip_count = 0");
writer.println("uncategorized_count = " + uncategorizedCount);
writer.println("runtime_call_name_count = 0");
writer.println("runtime_call_source = \"regex_only\"");
writer.println("runtime_call_name_count = " + RUNTIME_HANDLER_NAMES.size());
writer.println("runtime_call_source = \"embedded_ps2_call_list_snapshot\"");
}
println(String.format("Exported %d functions and %d executable labels to %s", functionCount, labelRecords.size(), csvFile.getAbsolutePath()));
println("Using regex-only runtime/library classification (no ps2_call_list.h).");
println(String.format("Using %d embedded runtime handler names from ps2_call_list.h snapshot.", RUNTIME_HANDLER_NAMES.size()));
println(String.format("Exported TOML config to %s", tomlFile.getAbsolutePath()));
}
}
+20 -57
View File
@@ -1,10 +1,10 @@
#include "MiniTest.h"
#include "ps2recomp/elf_analyzer.h"
#include "ps2recomp/function_classifier.h"
#include "ps2recomp/instructions.h"
#include "ps2recomp/types.h"
#include <unordered_map>
#include <unordered_set>
#include <vector>
using namespace ps2recomp;
@@ -47,9 +47,24 @@ void register_elf_analyzer_tests()
t.IsFalse(analyzer.isLibrarySymbolNameForHeuristics("bhEne13_Brain"),
"named game function should not be classified as library");
t.IsFalse(analyzer.isLibrarySymbolNameForHeuristics("ScenePrerender"),
"game functions beginning with Scene should not be classified as sce SDK APIs");
t.IsFalse(analyzer.isLibrarySymbolNameForHeuristics("sub_00100C00"),
"unreliable auto-generated names should not be classified as library"); });
tc.Run("runtime handler filter keeps unsupported SDK names informational", [](TestCase &t)
{
t.IsTrue(FunctionClassifier::hasRuntimeHandler("sceCdRead"),
"sceCdRead should resolve to a known runtime stub handler");
t.IsTrue(FunctionClassifier::hasRuntimeHandler("_printf"),
"runtime handler resolution should accept leading underscore aliases");
t.IsTrue(FunctionClassifier::hasRuntimeHandler("__ieee754_rem_pio2f"),
"double-underscore libm helpers should be active stubs only when the runtime knows them");
t.IsTrue(FunctionClassifier::hasRuntimeHandler("__kernel_cosf"),
"runtime-known libm kernel helpers should resolve exactly");
t.IsFalse(FunctionClassifier::hasRuntimeHandler("scePP1_Kick"),
"SDK functions without runtime handlers should not be active stubs"); });
tc.Run("reliable-symbol heuristic filters autogenerated names", [](TestCase &t)
{
t.IsTrue(ElfAnalyzer::isReliableSymbolNameForHeuristics("bhEne13_Brain"),
@@ -70,37 +85,6 @@ void register_elf_analyzer_tests()
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;
@@ -138,7 +122,7 @@ void register_elf_analyzer_tests()
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)
tc.Run("risk signal detection reports hardware io mmi and self modifying code", [](TestCase &t)
{
// Hardware I/O signal via LUI upper address in I/O region.
Instruction hw = makeInstruction(0x1000, OPCODE_LUI);
@@ -173,30 +157,9 @@ void register_elf_analyzer_tests()
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"); });
(void)hasHardwareIO;
(void)hasLargeComplexMMI;
(void)hasSelfModifying; });
tc.Run("jump-table detection finds canonical sltiu/bne/lw/jr pattern", [](TestCase &t)
{