mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 08:51:05 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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> §ions);
|
||||
static std::vector<JumpTable> detectJumpTables(
|
||||
const std::vector<Instruction> &instructions,
|
||||
const std::vector<Section> §ions,
|
||||
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
|
||||
@@ -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> §ions);
|
||||
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> §ions, 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> §ions) 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
|
||||
@@ -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> §ions)
|
||||
{
|
||||
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 §ion : 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> §ions,
|
||||
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 = [§ions](uint32_t addr) -> bool
|
||||
{
|
||||
if (addr == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sections.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const auto §ion : 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+375
-1142
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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> §ions) const
|
||||
{
|
||||
std::unordered_map<uint32_t, std::map<std::string, Candidate>> candidatesByAddress;
|
||||
|
||||
if (!m_root)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
for (const Section §ion : 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 §ion, 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 §ion, 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> §ions) const
|
||||
{
|
||||
return m_impl->scan(sections);
|
||||
}
|
||||
|
||||
const std::string &SceSymbolScanner::lastError() const
|
||||
{
|
||||
return m_impl->lastError();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
'''
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user