Feature/ghidra export and syscall fixes (#100)

* docs: deprecate the local analyzer workflow in favor of Ghidra
feat: improve the Ghidra exporter for stripped games and internal entry points
fix: correct FindAddress behavior in the runtime
fix: emit missing delay-slot code in recompiler  edge cases
feat: add SetSyscall support from @Whoneon
feat: add dispatchSyscallOverride support from @Whoneon
fix: fix  Unmatched '{' due to missing newlines from issue #96
feat: delete python ghidra script I never updated it anyway
feat: added a lot more of regression test
feat: added a lot of logs to help debug on runtime
fix: fix wrong syscall ID on runtime
This commit is contained in:
Ranieri
2026-03-09 00:41:51 -03:00
committed by GitHub
parent 9e4fd794c1
commit 7ca6a866c9
20 changed files with 1638 additions and 153 deletions
+18 -10
View File
@@ -61,27 +61,35 @@ cmake --build out/build --config Debug
### Usage
1. Analyze ELF and generate config:
Preferred workflow for retail or stripped games:
```bash
./ps2_analyzer your_game.elf config.toml
```
*For better results on retail games, see the [Ghidra Workflow](ps2xAnalyzer/Readme.md#3-ghidra-integration-recommended-for-complex-games).*
2. Recompile using generated TOML:
1. Open the ELF in Ghidra.
2. Run `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`.
3. Use the exported TOML and CSV map.
4. Recompile with the exported TOML:
```bash
./ps2_recomp config.toml
```
3. Build generated output and link with `ps2xRuntime`.
Fallback workflow for quick local experiments or ELFs with debug symbol :
```bash
./ps2_analyzer your_game.elf config.toml
```
Use this only when you do not have a Ghidra project yet. The native analyzer is faster to start, but it is less accurate on stripped retail games and more likely to miss internal callable entry points.
See the [Ghidra Workflow](ps2xAnalyzer/Readme.md#3-ghidra-integration-for-retail-and-stripped-games-preferred) for the recommended path.
Then build generated output and link with `ps2xRuntime`.
### Configuration
Main fields in `config.toml`:
* `general.input`: source ELF path.
* `general.ghidra_output`: optional function map CSV.
* `general.ghidra_output`: recommended function map CSV exported from Ghidra.
* `general.output`: generated C++ output folder.
* `general.single_file_output`: one combined cpp or one file per function.
* `general.patch_syscalls`: apply configured patches to `SYSCALL` instructions (`false` recommended).
@@ -96,7 +104,7 @@ Address binding for stripped ELFs:
* Use `handler@0xADDRESS` inside `general.stubs` to map a stripped function start directly to a runtime handler.
* Example: `sceCdRead@0x00123456` binds function start `0x00123456` to `ps2_stubs::sceCdRead(...)`.
* Generic temporary handlers are available: `ret0@0xADDR`, `ret1@0xADDR`, `reta0@0xADDR`.
* Before manual binding, try plain recompilation first: if ELF relocation symbols are present for calls, runtime handler routing can be inferred automatically.
* Before manual binding, prefer recompilation from a Ghidra-exported TOML/CSV first. The extra boundaries and synthetic entry points are usually more important than manual early triage.
* The address must be the function start in that exact ELF build.
* Addresses are not portable across different games/regions/builds.
* The handler name must exist in runtime call lists (`PS2_SYSCALL_LIST` or `PS2_STUB_LIST`).
+18 -11
View File
@@ -9,18 +9,21 @@ The analyzer supports three distinct paths for discovering code within a PS2 bin
### 1. DWARF Debug Information
If the ELF was compiled with debug symbols (`-g`), the analyzer uses `libdwarf` to extract perfect function names and exact start/end addresses. This is common in homebrew or early development builds.
### 2. Native Heuristic Scanner (Retail/Stripped)
### 2. Native Heuristic Scanner (Test only)
For commercial games where symbols are stripped, the analyzer uses a "JAL Scanner":
* It scans executable sections for `JAL` (Jump and Link) instructions.
* It infers function start points based on jump targets.
* It generates names like `sub_XXXXXXXX`.
### 3. Ghidra Integration (For Complex Games)
For the highest accuracy in stripped games, you can use Ghidra's superior analysis engine:
1. Use the provided script: `ps2xRecomp/tools/ghidra/ExportPS2Functions.py` or `.java`.
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)
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.
3. Add the CSV path to your TOML: `ghidra_output = "path/to/map.csv"`.
4. The recompiler will prioritize Ghidra's boundaries over its own heuristics.
3. Let the script generate the TOML, and keep the CSV path in `ghidra_output = "path/to/map.csv"`.
4. Run the recompiler with that exported TOML.
5. The recompiler will prioritize Ghidra's boundaries over its own heuristics.
## Key Features
@@ -41,12 +44,16 @@ ps2_analyzer <input_elf> <output_toml>
* `output_toml`: Path where the generated TOML configuration will be saved.
## Example Workflow
1. Run the analyzer on your game:
`ps2_analyzer game.elf config.toml`
2. (Optional) Open `game.elf` in Ghidra, run the export script, and update `config.toml` with the CSV path.
3. Run the recompiler:
1. Open `game.elf` in Ghidra.
2. Run `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`.
3. Use the exported TOML and CSV.
4. Run the recompiler:
`ps2recomp config.toml`
Fallback:
1. Run `ps2_analyzer game.elf config.toml`.
2. Use that TOML only for quick bring-up or symbol-rich builds.
## Generated Configuration
The tool creates a TOML file with the following sections:
* `[general]`: Paths to ELF and Ghidra maps.
@@ -60,4 +67,4 @@ The tool creates a TOML file with the following sections:
* Self-modifying code is flagged but requires manual review.
* Indirect jumps (jump tables) are detected but complex ones might need manual TOML entries.
For more details on the recompilation process, see the [Main README](../README.md).
For more details on the recompilation process, see the [Main README](../README.md).
+72 -45
View File
@@ -37,6 +37,16 @@ namespace ps2recomp
return ((address + 4) & 0xF0000000u) | (target << 2);
}
static Instruction makeSyntheticDelaySlot(uint32_t address)
{
Instruction inst{};
inst.address = address;
inst.raw = 0;
inst.opcode = OPCODE_SPECIAL;
inst.function = SPECIAL_SLL;
return inst;
}
static std::string formatFloatLiteral(float value)
{
if (!std::isfinite(value))
@@ -927,18 +937,35 @@ namespace ps2recomp
try
{
if (inst.hasDelaySlot && i + 1 < instructions.size())
if (inst.hasDelaySlot)
{
const Instruction &delaySlot = instructions[i + 1];
const bool hasDecodedDelaySlot =
i + 1 < instructions.size() &&
instructions[i + 1].address == inst.address + 4u;
if (internalTargets.contains(delaySlot.address))
Instruction syntheticDelaySlot{};
const Instruction *delaySlot = nullptr;
if (hasDecodedDelaySlot)
{
ss << "label_" << std::hex << delaySlot.address << std::dec << ":\n";
delaySlot = &instructions[i + 1];
}
else
{
syntheticDelaySlot = makeSyntheticDelaySlot(inst.address + 4u);
delaySlot = &syntheticDelaySlot;
}
ss << handleBranchDelaySlots(inst, delaySlot, function, analysisResult);
if (hasDecodedDelaySlot && internalTargets.contains(delaySlot->address))
{
ss << "label_" << std::hex << delaySlot->address << std::dec << ":\n";
}
++i; // Skip delay slot instruction (handled inside branch logic)
ss << handleBranchDelaySlots(inst, *delaySlot, function, analysisResult);
if (hasDecodedDelaySlot)
{
++i; // Skip delay slot instruction (handled inside branch logic)
}
}
else
{
@@ -2949,18 +2976,18 @@ namespace ps2recomp
std::string CodeGenerator::translateVU_VRNEXT(const Instruction &inst)
{
return fmt::format(
"{{ "
" uint32_t r_vals[4]; "
" _mm_storeu_si128((__m128i*)r_vals, _mm_castps_si128(ctx->vu0_r)); "
" "
" // Simple LFSR-based random number generation (PS2-like behavior) "
" uint32_t feedback = r_vals[0] ^ (r_vals[0] << 13) ^ (r_vals[1] >> 19) ^ (r_vals[2] << 7); "
" r_vals[0] = r_vals[1]; "
" r_vals[1] = r_vals[2]; "
" r_vals[2] = r_vals[3]; "
" r_vals[3] = feedback; "
" "
" ctx->vu0_r = _mm_castsi128_ps(_mm_loadu_si128((__m128i*)r_vals)); \n"
"{{\n"
" uint32_t r_vals[4];\n"
" _mm_storeu_si128((__m128i*)r_vals, _mm_castps_si128(ctx->vu0_r));\n"
"\n"
" // Simple LFSR-based random number generation (PS2-like behavior)\n"
" uint32_t feedback = r_vals[0] ^ (r_vals[0] << 13) ^ (r_vals[1] >> 19) ^ (r_vals[2] << 7);\n"
" r_vals[0] = r_vals[1];\n"
" r_vals[1] = r_vals[2];\n"
" r_vals[2] = r_vals[3];\n"
" r_vals[3] = feedback;\n"
"\n"
" ctx->vu0_r = _mm_castsi128_ps(_mm_loadu_si128((__m128i*)r_vals));\n"
"}}");
}
@@ -3608,19 +3635,19 @@ namespace ps2recomp
uint8_t fsf = inst.vectorInfo.fsf;
return fmt::format(
"{{ "
" float src = _mm_cvtss_f32(_mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(0,0,0,{}))); "
" uint32_t seed; std::memcpy(&seed, &src, sizeof(seed)); "
" "
" // PS2 uses a specific LFSR initialization pattern "
" if (seed == 0) seed = 1; " // Prevent zero seed
" "
" uint32_t r0 = seed; "
" uint32_t r1 = seed * 0x41C64E6D + 0x3039; " // PS2-like LCG constants
" uint32_t r2 = r1 * 0x41C64E6D + 0x3039; "
" uint32_t r3 = r2 * 0x41C64E6D + 0x3039; "
" "
" ctx->vu0_r = _mm_castsi128_ps(_mm_set_epi32(r3, r2, r1, r0)); \n "
"{{\n"
" float src = _mm_cvtss_f32(_mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(0,0,0,{})));\n"
" uint32_t seed; std::memcpy(&seed, &src, sizeof(seed));\n"
"\n"
" // PS2 uses a specific LFSR initialization pattern\n"
" if (seed == 0) seed = 1;\n"
"\n"
" uint32_t r0 = seed;\n"
" uint32_t r1 = seed * 0x41C64E6D + 0x3039;\n"
" uint32_t r2 = r1 * 0x41C64E6D + 0x3039;\n"
" uint32_t r3 = r2 * 0x41C64E6D + 0x3039;\n"
"\n"
" ctx->vu0_r = _mm_castsi128_ps(_mm_set_epi32(r3, r2, r1, r0));\n"
"}}",
fs_reg, fs_reg, fsf);
}
@@ -3631,20 +3658,20 @@ namespace ps2recomp
uint8_t fsf = inst.vectorInfo.fsf;
return fmt::format(
"{{ "
" float src = _mm_cvtss_f32(_mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(0,0,0,{}))); "
" uint32_t src_bits; std::memcpy(&src_bits, &src, sizeof(src_bits)); "
" __m128i r_current = _mm_castps_si128(ctx->vu0_r); "
" __m128i fs_data = _mm_set1_epi32((int)src_bits); "
" "
" // XOR the current random value with the data from the VU vector register "
" __m128i xored = _mm_xor_si128(r_current, fs_data); "
" "
" // Apply a simple mixing function similar to PS2's LFSR "
" __m128i mixed = _mm_xor_si128(xored, _mm_slli_epi32(xored, 7)); "
" mixed = _mm_xor_si128(mixed, _mm_srli_epi32(mixed, 9)); "
" "
" ctx->vu0_r = _mm_castsi128_ps(mixed);"
"{{\n"
" float src = _mm_cvtss_f32(_mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(0,0,0,{})));\n"
" uint32_t src_bits; std::memcpy(&src_bits, &src, sizeof(src_bits));\n"
" __m128i r_current = _mm_castps_si128(ctx->vu0_r);\n"
" __m128i fs_data = _mm_set1_epi32((int)src_bits);\n"
"\n"
" // XOR the current random value with the data from the VU vector register\n"
" __m128i xored = _mm_xor_si128(r_current, fs_data);\n"
"\n"
" // Apply a simple mixing function similar to PS2's LFSR\n"
" __m128i mixed = _mm_xor_si128(xored, _mm_slli_epi32(xored, 7));\n"
" mixed = _mm_xor_si128(mixed, _mm_srli_epi32(mixed, 9));\n"
"\n"
" ctx->vu0_r = _mm_castsi128_ps(mixed);\n"
"}}",
fs_reg, fs_reg, fsf);
}
+31 -6
View File
@@ -241,6 +241,12 @@ namespace ps2recomp
size_t passCount = 0;
};
struct StaticEntryTarget
{
uint32_t target = 0u;
bool isCall = false;
};
EntryDiscoveryStats discoverAdditionalEntryPointsImpl(
std::vector<Function> &functions,
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
@@ -269,11 +275,14 @@ namespace ps2recomp
return false;
};
auto getStaticEntryTarget = [](const Instruction &inst) -> std::optional<uint32_t>
auto getStaticEntryTarget = [](const Instruction &inst) -> std::optional<StaticEntryTarget>
{
if (inst.opcode == OPCODE_J || inst.opcode == OPCODE_JAL)
{
return decodeAbsoluteJumpTarget(inst.address, inst.target);
StaticEntryTarget target{};
target.target = decodeAbsoluteJumpTarget(inst.address, inst.target);
target.isCall = (inst.opcode == OPCODE_JAL);
return target;
}
if (inst.opcode == OPCODE_SPECIAL &&
@@ -369,7 +378,8 @@ namespace ps2recomp
continue;
}
uint32_t target = targetOpt.value();
const StaticEntryTarget staticTarget = targetOpt.value();
const uint32_t target = staticTarget.target;
if ((target & 0x3) != 0 || !isExecutableAddress(target))
{
@@ -381,10 +391,25 @@ namespace ps2recomp
continue;
}
const Function *containingFunction = findContainingFunction(target);
if (containingFunction && containingFunction->start == function.start)
const bool targetInCurrentFunction = std::any_of(
instructions.begin(), instructions.end(),
[&](const Instruction &candidate)
{ return candidate.address == target; });
if (targetInCurrentFunction && !staticTarget.isCall)
{
// Internal branches within the same function are handled as labels/gotos and should not produce separate entry wrappers.
// jumps with the current decoded function remain labels/gotos.
continue;
}
const Function *containingFunction = findContainingFunction(target);
if (targetInCurrentFunction)
{
PendingEntry pending{};
pending.target = target;
pending.containingStart = function.start;
pending.containingEnd = function.end;
pendingEntries.push_back(pending);
pendingStarts.insert(target);
continue;
}
+210 -25
View File
@@ -2,10 +2,21 @@
// @category PS2Recomp
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.listing.Instruction;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionIterator;
import ghidra.program.model.listing.FunctionManager;
import ghidra.program.model.listing.InstructionIterator;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.symbol.ReferenceIterator;
import ghidra.program.model.symbol.RefType;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.SymbolIterator;
import ghidra.program.model.symbol.SymbolType;
import java.io.File;
import java.io.PrintWriter;
@@ -91,6 +102,7 @@ public class ExportPS2Functions extends GhidraScript {
long start;
long endExclusive;
long size;
boolean syntheticEntry = false;
}
private enum ClassificationKind {
@@ -331,6 +343,178 @@ public class ExportPS2Functions extends GhidraScript {
return selectors;
}
private boolean isExecutableAddress(Address address) {
if (address == null) {
return false;
}
MemoryBlock block = currentProgram.getMemory().getBlock(address);
return block != null && block.isExecute();
}
private boolean hasCallableLabelReference(Address address) {
if (address == null) {
return false;
}
ReferenceIterator refs = currentProgram.getReferenceManager().getReferencesTo(address);
while (refs.hasNext()) {
Reference ref = refs.next();
if (ref == null) {
continue;
}
RefType type = ref.getReferenceType();
if (type != null && type.isCall()) {
return true;
}
Address from = ref.getFromAddress();
if (from == null) {
continue;
}
MemoryBlock fromBlock = currentProgram.getMemory().getBlock(from);
if (fromBlock == null || !fromBlock.isExecute()) {
return true;
}
}
return false;
}
private static String makeAnonymousEntryName(long start) {
return String.format("entry_%08x", start & 0xFFFFFFFFL);
}
private List<FunctionRecord> collectExecutableLabelRecords(List<FunctionRecord> functionRecords) {
List<FunctionRecord> labelRecords = new ArrayList<>();
Set<Long> existingStarts = new HashSet<>();
for (FunctionRecord record : functionRecords) {
existingStarts.add(record.start);
}
SymbolIterator symbols = currentProgram.getSymbolTable().getSymbolIterator(true);
while (symbols.hasNext() && !monitor.isCancelled()) {
Symbol symbol = symbols.next();
if (symbol == null || !symbol.isPrimary()) {
continue;
}
if (symbol.getSymbolType() == SymbolType.FUNCTION) {
continue;
}
Address address = symbol.getAddress();
if (!isExecutableAddress(address)) {
continue;
}
long start = address.getOffset();
if (existingStarts.contains(start)) {
continue;
}
Instruction instruction = currentProgram.getListing().getInstructionAt(address);
if (instruction == null) {
continue;
}
if (!hasCallableLabelReference(address)) {
continue;
}
FunctionRecord record = new FunctionRecord();
record.name = symbol.getName();
record.start = start;
record.syntheticEntry = true;
labelRecords.add(record);
existingStarts.add(start);
}
AddressSet executableAddresses = new AddressSet();
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
if (block != null && block.isExecute()) {
executableAddresses.addRange(block.getStart(), block.getEnd());
}
}
InstructionIterator instructions = currentProgram.getListing().getInstructions(executableAddresses, true);
while (instructions.hasNext() && !monitor.isCancelled()) {
Instruction instruction = instructions.next();
if (instruction == null) {
continue;
}
Address address = instruction.getAddress();
long start = address.getOffset();
if (existingStarts.contains(start)) {
continue;
}
if (!hasCallableLabelReference(address)) {
continue;
}
FunctionRecord record = new FunctionRecord();
record.name = makeAnonymousEntryName(start);
record.start = start;
record.syntheticEntry = true;
labelRecords.add(record);
existingStarts.add(start);
}
if (labelRecords.isEmpty()) {
return labelRecords;
}
List<Long> boundaries = new ArrayList<>();
for (FunctionRecord record : functionRecords) {
boundaries.add(record.start);
}
for (FunctionRecord record : labelRecords) {
boundaries.add(record.start);
}
Collections.sort(boundaries);
functionRecords.sort(Comparator.comparingLong(r -> r.start));
for (FunctionRecord record : labelRecords) {
long endExclusive = 0L;
for (FunctionRecord functionRecord : functionRecords) {
if (record.start > functionRecord.start && record.start < functionRecord.endExclusive) {
endExclusive = functionRecord.endExclusive;
break;
}
}
if (endExclusive == 0L) {
Address startAddress = currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(record.start);
MemoryBlock block = currentProgram.getMemory().getBlock(startAddress);
if (block != null) {
endExclusive = block.getEnd().getOffset() + 1L;
}
}
for (Long boundary : boundaries) {
if (boundary > record.start && (endExclusive == 0L || boundary < endExclusive)) {
endExclusive = boundary;
break;
}
}
if (endExclusive <= record.start) {
endExclusive = record.start + 4L;
}
record.endExclusive = endExclusive;
record.size = record.endExclusive - record.start;
}
labelRecords.sort(Comparator.comparingLong(r -> r.start));
return labelRecords;
}
@Override
public void run() throws Exception {
File tomlFile = askFile("Choose output TOML config file", "Save");
@@ -340,11 +524,9 @@ public class ExportPS2Functions extends GhidraScript {
boolean exportCsv = askYesNo("Export CSV", "Also export compatibility CSV function map?");
File csvFile = null;
if (exportCsv) {
csvFile = askFile("Choose output CSV file", "Save");
if (csvFile == null) {
exportCsv = false;
}
csvFile = askFile("Choose output CSV file", "Save");
if (csvFile == null) {
return;
}
FunctionManager fm = currentProgram.getFunctionManager();
@@ -380,23 +562,26 @@ public class ExportPS2Functions extends GhidraScript {
}
}
List<String> stubSelectors = collectFunctionSelectors(stubNames, functionRecords, true);
List<String> skipSelectors = collectFunctionSelectors(skipNames, functionRecords, true);
final int functionCount = functionRecords.size();
List<FunctionRecord> labelRecords = collectExecutableLabelRecords(functionRecords);
List<FunctionRecord> exportRecords = new ArrayList<>(functionRecords);
exportRecords.addAll(labelRecords);
exportRecords.sort(Comparator.comparingLong(r -> r.start));
if (exportCsv && csvFile != null) {
try (PrintWriter writer = new PrintWriter(csvFile)) {
writer.println("Name,Start,End,Size");
functionRecords.sort(Comparator.comparingLong(r -> r.start));
for (FunctionRecord record : functionRecords) {
writer.printf("%s,0x%08X,0x%08X,%d%n",
record.name,
record.start,
record.endExclusive,
record.size
);
}
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) {
writer.printf("%s,0x%08X,0x%08X,%d%n",
record.name,
record.start,
record.endExclusive,
record.size
);
}
}
}
String programPath = currentProgram.getExecutablePath();
if (programPath == null) {
@@ -405,7 +590,7 @@ public class ExportPS2Functions extends GhidraScript {
programPath = normalizeWindowsDrivePath(programPath);
File outputDir = tomlFile.getParentFile() == null ? new File("output") : new File(tomlFile.getParentFile(), "output");
String ghidraCsvPath = (exportCsv && csvFile != null) ? csvFile.getAbsolutePath() : "";
String ghidraCsvPath = csvFile.getAbsolutePath();
try (PrintWriter writer = new PrintWriter(tomlFile)) {
writer.println("# Auto-generated by ExportPS2Functions.java");
@@ -437,7 +622,9 @@ public class ExportPS2Functions extends GhidraScript {
writer.println();
writer.println("[ghidra_export]");
writer.println("function_count = " + functionRecords.size());
writer.println("function_count = " + functionCount);
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("uncategorized_count = " + uncategorizedCount);
@@ -445,9 +632,7 @@ public class ExportPS2Functions extends GhidraScript {
writer.println("runtime_call_source = \"regex_only\"");
}
if (exportCsv && csvFile != null) {
println(String.format("Exported %d functions to %s", functionRecords.size(), csvFile.getAbsolutePath()));
}
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("Exported TOML config to %s", tomlFile.getAbsolutePath()));
@@ -1,43 +0,0 @@
# Exports function addresses and names to CSV for PS2Recomp
# @category PS2Recomp
import csv
import os
from ghidra.program.model.symbol import SourceType
def run():
f = askFile("Choose output CSV file", "Save")
if f is None:
return
with open(f.getAbsolutePath(), 'w') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['Name', 'Start', 'End', 'Size'])
fm = currentProgram.getFunctionManager()
functions = fm.getFunctions(True) # True iterates forward wtf kkkkk
count = 0
for func in functions:
name = func.getName()
start = func.getEntryPoint().getOffset()
body = func.getBody()
max_addr = body.getMaxAddress().getOffset()
size = body.getNumAddresses()
writer.writerow([
name,
"0x{:08X}".format(start),
"0x{:08X}".format(max_addr + 1), # End address is exclusive
size
])
count += 1
print("Exported {} functions to {}".format(count, f.getAbsolutePath()))
if __name__ == "__main__":
run()
+1
View File
@@ -107,6 +107,7 @@
X(GsPutIMR) \
X(iGsPutIMR) \
X(SetVSyncFlag) \
X(SetSyscall) \
X(GsSetVideoMode) \
\
X(GetOsdConfigParam) \
+2
View File
@@ -22,6 +22,8 @@ namespace ps2_syscalls
#undef PS2_DECLARE_SYSCALL
bool dispatchNumericSyscall(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
void dispatchDmacHandlersForCause(uint8_t *rdram, PS2Runtime *runtime, uint32_t cause);
void initializeGuestKernelState(uint8_t *rdram);
void TODO(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime, uint32_t encodedSyscallId);
void notifyRuntimeStop();
void WaitVSyncTick(uint8_t *rdram, PS2Runtime *runtime);
+7
View File
@@ -19,6 +19,11 @@
#include "ps2_gs_gpu.h"
#include <ThreadNaming.h>
namespace ps2_stubs
{
void resetSifState();
}
#define ELF_MAGIC 0x464C457F // "\x7FELF" in little endian
#define ET_EXEC 2 // Executable file
#define EM_MIPS 8 // MIPS architecture
@@ -1701,7 +1706,9 @@ void PS2Runtime::HandleIntegerOverflow(R5900Context *ctx)
void PS2Runtime::run()
{
m_stopRequested.store(false, std::memory_order_relaxed);
ps2_stubs::resetSifState();
ps2_stubs::resetGsSyncVCallbackState();
ps2_syscalls::initializeGuestKernelState(m_memory.getRDRAM());
m_cpuContext.r[4] = _mm_setzero_si128();
m_cpuContext.r[5] = _mm_setzero_si128();
m_cpuContext.r[29] = _mm_set_epi64x(0, static_cast<int64_t>(PS2_RAM_SIZE - 0x10u));
+16 -1
View File
@@ -12,6 +12,7 @@
#include <fstream>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <thread>
#include <condition_variable>
#include <atomic>
@@ -42,6 +43,11 @@ namespace ps2_syscalls
bool dispatchNumericSyscall(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
if (dispatchSyscallOverride(syscallNumber, rdram, ctx, runtime))
{
return true;
}
switch (syscallNumber)
{
case 0x01:
@@ -272,7 +278,7 @@ namespace ps2_syscalls
SetVSyncFlag(rdram, ctx, runtime);
return true;
case 0x74:
RegisterExitHandler(rdram, ctx, runtime);
SetSyscall(rdram, ctx, runtime);
return true;
case 0x76:
case static_cast<uint32_t>(-0x76):
@@ -391,5 +397,14 @@ namespace ps2_syscalls
g_alarms.clear();
}
g_alarm_cv.notify_all();
{
std::lock_guard<std::mutex> lock(g_exit_handler_mutex);
g_exit_handlers.clear();
}
{
std::lock_guard<std::mutex> lock(g_syscall_override_mutex);
g_syscall_overrides.clear();
}
}
}
+91 -2
View File
@@ -2627,6 +2627,55 @@ namespace
uint32_t g_sifCmdBuffer = 0u;
uint32_t g_sifSysCmdBuffer = 0u;
bool g_sifCmdInitialized = false;
uint32_t g_sifGetRegLogCount = 0u;
uint32_t g_sifSetRegLogCount = 0u;
constexpr uint32_t kSifRegBootStatus = 0x4u;
constexpr uint32_t kSifRegMainAddr = 0x80000000u;
constexpr uint32_t kSifRegSubAddr = 0x80000001u;
constexpr uint32_t kSifRegMsCom = 0x80000002u;
constexpr uint32_t kSifBootReadyMask = 0x00020000u;
void seedDefaultSifRegsLocked()
{
g_sifRegs.clear();
g_sifSregs.clear();
g_sifCmdHandlers.clear();
g_sifCmdBuffer = 0u;
g_sifSysCmdBuffer = 0u;
g_sifCmdInitialized = false;
g_sifGetRegLogCount = 0u;
g_sifSetRegLogCount = 0u;
g_sifRegs[kSifRegBootStatus] = kSifBootReadyMask;
g_sifRegs[kSifRegMainAddr] = 0u;
g_sifRegs[kSifRegSubAddr] = 0u;
g_sifRegs[kSifRegMsCom] = 0u;
}
bool shouldTraceSifReg(uint32_t reg)
{
switch (reg)
{
case 0x2u:
case 0x4u:
case 0x80000000u:
case 0x80000001u:
case 0x80000002u:
return true;
default:
return false;
}
}
struct SifStateInitializer
{
SifStateInitializer()
{
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
seedDefaultSifRegsLocked();
}
} g_sifStateInitializer;
uint32_t allocateSifDmaTransferId()
{
@@ -2744,6 +2793,12 @@ namespace
}
}
void resetSifState()
{
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
seedDefaultSifRegsLocked();
}
void sceSifAddCmdHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
const uint32_t cid = getRegU32(ctx, 4);
@@ -2796,8 +2851,7 @@ void sceSifExecRequest(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
void sceSifExitCmd(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
g_sifCmdInitialized = false;
g_sifCmdHandlers.clear();
seedDefaultSifRegsLocked();
setReturnS32(ctx, 0);
}
@@ -3054,6 +3108,7 @@ void sceSifGetReg(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
const uint32_t reg = getRegU32(ctx, 4);
uint32_t value = 0u;
bool shouldLog = false;
{
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
auto it = g_sifRegs.find(reg);
@@ -3061,6 +3116,21 @@ void sceSifGetReg(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
value = it->second;
}
shouldLog = shouldTraceSifReg(reg) && g_sifGetRegLogCount < 128u;
if (shouldLog)
{
++g_sifGetRegLogCount;
}
}
if (shouldLog)
{
auto flags = std::cerr.flags();
std::cerr << "[sceSifGetReg] reg=0x" << std::hex << reg
<< " value=0x" << value
<< " pc=0x" << (ctx ? ctx->pc : 0u)
<< " ra=0x" << (ctx ? getRegU32(ctx, 31) : 0u)
<< std::dec << std::endl;
std::cerr.flags(flags);
}
setReturnU32(ctx, value);
}
@@ -3259,6 +3329,8 @@ void sceSifSetDma(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
return;
}
ps2_syscalls::dispatchDmacHandlersForCause(rdram, runtime, 5u);
setReturnS32(ctx, static_cast<int32_t>(allocateSifDmaTransferId()));
}
@@ -3272,6 +3344,7 @@ void sceSifSetReg(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
const uint32_t reg = getRegU32(ctx, 4);
const uint32_t value = getRegU32(ctx, 5);
uint32_t prev = 0u;
bool shouldLog = false;
{
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
auto it = g_sifRegs.find(reg);
@@ -3280,6 +3353,22 @@ void sceSifSetReg(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
prev = it->second;
}
g_sifRegs[reg] = value;
shouldLog = shouldTraceSifReg(reg) && g_sifSetRegLogCount < 128u;
if (shouldLog)
{
++g_sifSetRegLogCount;
}
}
if (shouldLog)
{
auto flags = std::cerr.flags();
std::cerr << "[sceSifSetReg] reg=0x" << std::hex << reg
<< " prev=0x" << prev
<< " value=0x" << value
<< " pc=0x" << (ctx ? ctx->pc : 0u)
<< " ra=0x" << (ctx ? getRegU32(ctx, 31) : 0u)
<< std::dec << std::endl;
std::cerr.flags(flags);
}
setReturnU32(ctx, prev);
}
@@ -101,9 +101,9 @@ static std::shared_ptr<EventFlagInfo> lookupEventFlagInfo(int eid)
static void setRegU32(R5900Context *ctx, int reg, uint32_t value)
{
if (reg < 0 || reg > 31)
if (!ctx || reg < 0 || reg > 31)
return;
ctx->r[reg] = _mm_set_epi32(0, 0, 0, value);
SET_GPR_U32(ctx, reg, value);
}
static std::chrono::microseconds alarmTicksToDuration(uint16_t ticks)
@@ -268,6 +268,34 @@ static bool readStackU32(uint8_t *rdram, uint32_t sp, uint32_t offset, uint32_t
return true;
}
enum class RpcInvokeExitReason
{
Returned,
NullPc,
MissingFunction,
StepLimit,
SamePcLimit
};
static const char *rpcInvokeExitReasonName(RpcInvokeExitReason reason)
{
switch (reason)
{
case RpcInvokeExitReason::Returned:
return "returned";
case RpcInvokeExitReason::NullPc:
return "null-pc";
case RpcInvokeExitReason::MissingFunction:
return "missing-function";
case RpcInvokeExitReason::StepLimit:
return "step-limit";
case RpcInvokeExitReason::SamePcLimit:
return "same-pc-limit";
default:
return "unknown";
}
}
static bool rpcInvokeFunction(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime,
uint32_t funcAddr, uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t *outV0)
{
@@ -307,6 +335,7 @@ static bool rpcInvokeFunction(uint8_t *rdram, R5900Context *ctx, PS2Runtime *run
uint32_t steps = 0u;
uint32_t lastPc = 0xFFFFFFFFu;
uint32_t samePcCount = 0u;
RpcInvokeExitReason exitReason = RpcInvokeExitReason::MissingFunction;
while (tmp.pc != 0u &&
tmp.pc != kRpcInvokeReturnSentinel &&
runtime->hasFunction(tmp.pc) &&
@@ -318,6 +347,7 @@ static bool rpcInvokeFunction(uint8_t *rdram, R5900Context *ctx, PS2Runtime *run
++samePcCount;
if (samePcCount > 0x2000u)
{
exitReason = RpcInvokeExitReason::SamePcLimit;
break;
}
}
@@ -336,7 +366,41 @@ static bool rpcInvokeFunction(uint8_t *rdram, R5900Context *ctx, PS2Runtime *run
{
*outV0 = getRegU32(&tmp, 2);
}
return true;
if (tmp.pc == kRpcInvokeReturnSentinel)
{
return true;
}
if (tmp.pc == 0u)
{
exitReason = RpcInvokeExitReason::NullPc;
}
else if (steps >= kRpcInvokeMaxSteps)
{
exitReason = RpcInvokeExitReason::StepLimit;
}
else if (!runtime->hasFunction(tmp.pc))
{
exitReason = RpcInvokeExitReason::MissingFunction;
}
static std::atomic<uint32_t> s_rpcInvokeFailureLogs{0u};
constexpr uint32_t kMaxRpcInvokeFailureLogs = 64u;
const uint32_t logIndex = s_rpcInvokeFailureLogs.fetch_add(1u, std::memory_order_relaxed);
if (logIndex < kMaxRpcInvokeFailureLogs)
{
std::cerr << "[SyscallOverride:invoke-failed]"
<< " func=0x" << std::hex << funcAddr
<< " exitPc=0x" << tmp.pc
<< " ra=0x" << getRegU32(&tmp, 31)
<< std::dec
<< " steps=" << steps
<< " reason=" << rpcInvokeExitReasonName(exitReason)
<< std::endl;
}
return false;
}
static uint32_t rpcAllocPacketAddr(uint8_t *rdram)
@@ -416,6 +416,15 @@ static bool g_bootmode_initialized = false;
static uint32_t g_bootmode_pool_offset = 0;
static std::unordered_map<uint8_t, uint32_t> g_bootmode_addresses;
static std::mutex g_syscall_override_mutex;
static std::unordered_map<uint32_t, uint32_t> g_syscall_overrides;
static std::unordered_set<uint32_t> g_syscall_mirror_addrs;
static constexpr uint32_t kGuestSyscallTableGuestBase = 0x80011F80u;
static constexpr uint32_t kGuestSyscallTablePhysBase = kGuestSyscallTableGuestBase & 0x1FFFFFFFu;
static constexpr uint32_t kGuestSyscallMirrorLimit = 0x00080000u;
static constexpr uint32_t kGuestSyscallTableProbeBase = 0x000002F0u;
static std::mutex g_tls_mutex;
static uint32_t g_tls_index = 0;
@@ -138,6 +138,90 @@ static void dispatchIntcHandlersForCause(uint8_t *rdram, PS2Runtime *runtime, ui
}
}
void dispatchDmacHandlersForCause(uint8_t *rdram, PS2Runtime *runtime, uint32_t cause)
{
if (!rdram || !runtime)
{
return;
}
std::vector<IrqHandlerInfo> handlers;
{
std::lock_guard<std::mutex> lock(g_irq_handler_mutex);
if (cause < 32u && (g_enabled_dmac_mask & (1u << cause)) == 0u)
{
return;
}
handlers.reserve(g_dmacHandlers.size());
for (const auto &[id, info] : g_dmacHandlers)
{
(void)id;
if (!info.enabled)
{
continue;
}
if (info.cause != cause)
{
continue;
}
if (info.handler == 0u)
{
continue;
}
handlers.push_back(info);
}
std::sort(handlers.begin(), handlers.end(), [](const IrqHandlerInfo &a, const IrqHandlerInfo &b) {
return a.order < b.order;
});
}
for (const IrqHandlerInfo &info : handlers)
{
if (!runtime->hasFunction(info.handler))
{
continue;
}
try
{
R5900Context irqCtx{};
const uint32_t sp = (info.sp != 0u) ? info.sp : (PS2_RAM_SIZE - 0x10u);
SET_GPR_U32(&irqCtx, 28, info.gp);
SET_GPR_U32(&irqCtx, 29, sp);
SET_GPR_U32(&irqCtx, 31, 0u);
SET_GPR_U32(&irqCtx, 4, cause);
SET_GPR_U32(&irqCtx, 5, info.arg);
SET_GPR_U32(&irqCtx, 6, 0u);
SET_GPR_U32(&irqCtx, 7, 0u);
irqCtx.pc = info.handler;
while (irqCtx.pc != 0u && runtime && !runtime->isStopRequested())
{
PS2Runtime::RecompiledFunction step = runtime->lookupFunction(irqCtx.pc);
if (!step)
{
break;
}
step(rdram, &irqCtx, runtime);
}
}
catch (const ThreadExitException &)
{
}
catch (const std::exception &e)
{
static uint32_t warnCount = 0;
if (warnCount < 8u)
{
std::cerr << "[DMAC] handler 0x" << std::hex << info.handler
<< " threw exception: " << e.what() << std::dec << std::endl;
++warnCount;
}
}
}
}
static uint64_t signalVSyncFlag(uint8_t *rdram)
{
VSyncFlagRegistration reg{};
@@ -264,6 +264,239 @@ void TODO(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime, uint32_t encod
setReturnS32(ctx, 0);
}
static uint32_t computeBuiltinFindAddressResult(uint8_t *rdram,
uint32_t originalStart,
uint32_t originalEnd,
uint32_t target);
static bool dispatchSyscallOverride(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
uint32_t handler = 0u;
{
std::lock_guard<std::mutex> lock(g_syscall_override_mutex);
auto it = g_syscall_overrides.find(syscallNumber);
if (it == g_syscall_overrides.end())
{
return false;
}
handler = it->second;
}
if (!runtime || !ctx || handler == 0u)
{
return false;
}
const uint32_t overrideA0 = getRegU32(ctx, 4);
const uint32_t overrideA1 = getRegU32(ctx, 5);
const uint32_t overrideA2 = getRegU32(ctx, 6);
const uint32_t overrideA3 = getRegU32(ctx, 7);
const uint32_t overridePc = ctx->pc;
const uint32_t overrideRa = getRegU32(ctx, 31);
thread_local std::vector<uint32_t> s_activeSyscallOverrides;
if (std::find(s_activeSyscallOverrides.begin(), s_activeSyscallOverrides.end(), syscallNumber) != s_activeSyscallOverrides.end())
{
static std::atomic<uint32_t> s_reentrantLogs{0u};
constexpr uint32_t kMaxReentrantLogs = 32u;
const uint32_t logIndex = s_reentrantLogs.fetch_add(1u, std::memory_order_relaxed);
if (logIndex < kMaxReentrantLogs)
{
std::cerr << "[SyscallOverride:reentrant]"
<< " syscall=0x" << std::hex << syscallNumber
<< " handler=0x" << handler
<< " pc=0x" << ctx->pc
<< " ra=0x" << getRegU32(ctx, 31)
<< std::dec << std::endl;
}
return false;
}
s_activeSyscallOverrides.push_back(syscallNumber);
struct ScopedActiveOverride
{
std::vector<uint32_t> &active;
~ScopedActiveOverride()
{
if (!active.empty())
{
active.pop_back();
}
}
} scopedActiveOverride{s_activeSyscallOverrides};
uint32_t retV0 = 0u;
const bool invoked = rpcInvokeFunction(rdram,
ctx,
runtime,
handler,
getRegU32(ctx, 4),
getRegU32(ctx, 5),
getRegU32(ctx, 6),
getRegU32(ctx, 7),
&retV0);
if (syscallNumber == 0x83u)
{
const uint32_t builtinRet = computeBuiltinFindAddressResult(rdram, overrideA0, overrideA1, overrideA2);
const bool mismatch = (retV0 != builtinRet);
static std::atomic<uint32_t> s_findAddressOverrideLogs{0u};
static std::atomic<uint32_t> s_findAddressOverrideMismatchLogs{0u};
constexpr uint32_t kMaxFindAddressOverrideLogs = 64u;
constexpr uint32_t kMaxFindAddressOverrideMismatchLogs = 128u;
const uint32_t logIndex = s_findAddressOverrideLogs.fetch_add(1u, std::memory_order_relaxed);
const uint32_t mismatchIndex = mismatch
? s_findAddressOverrideMismatchLogs.fetch_add(1u, std::memory_order_relaxed)
: 0u;
if (logIndex < kMaxFindAddressOverrideLogs ||
(mismatch && mismatchIndex < kMaxFindAddressOverrideMismatchLogs))
{
const uint32_t guestMinus20c = (retV0 != 0u) ? (retV0 - 0x20Cu) : 0u;
const uint32_t guestMinus168 = (retV0 != 0u) ? (retV0 - 0x168u) : 0u;
const uint32_t builtinMinus20c = (builtinRet != 0u) ? (builtinRet - 0x20Cu) : 0u;
const uint32_t builtinMinus168 = (builtinRet != 0u) ? (builtinRet - 0x168u) : 0u;
std::cerr << "[Syscall83:override]"
<< " handler=0x" << std::hex << handler
<< " invoked=" << (invoked ? "true" : "false")
<< " pc=0x" << overridePc
<< " ra=0x" << overrideRa
<< " a0=0x" << overrideA0
<< " a1=0x" << overrideA1
<< " a2=0x" << overrideA2
<< " a3=0x" << overrideA3
<< " guestRet=0x" << retV0
<< " builtinRet=0x" << builtinRet
<< " guest-20c=0x" << guestMinus20c
<< " builtin-20c=0x" << builtinMinus20c
<< " guest-168=0x" << guestMinus168
<< " builtin-168=0x" << builtinMinus168
<< " match=" << (mismatch ? "false" : "true")
<< std::dec << std::endl;
}
}
if (!invoked)
{
static std::atomic<uint32_t> s_fallbackLogs{0u};
constexpr uint32_t kMaxFallbackLogs = 64u;
const uint32_t logIndex = s_fallbackLogs.fetch_add(1u, std::memory_order_relaxed);
if (logIndex < kMaxFallbackLogs)
{
std::cerr << "[SyscallOverride:fallback]"
<< " syscall=0x" << std::hex << syscallNumber
<< " handler=0x" << handler
<< " pc=0x" << ctx->pc
<< " ra=0x" << getRegU32(ctx, 31)
<< std::dec << std::endl;
}
return false;
}
setReturnU32(ctx, retV0);
return true;
}
static bool tryResolveGuestSyscallMirrorAddr(uint32_t syscallIndex, uint32_t &guestAddr)
{
const int64_t offsetBytes =
static_cast<int64_t>(static_cast<int32_t>(syscallIndex)) * static_cast<int64_t>(sizeof(uint32_t));
const int64_t guestAddr64 = static_cast<int64_t>(kGuestSyscallTablePhysBase) + offsetBytes;
if (guestAddr64 < 0 || (guestAddr64 + static_cast<int64_t>(sizeof(uint32_t))) > static_cast<int64_t>(kGuestSyscallMirrorLimit))
{
return false;
}
guestAddr = static_cast<uint32_t>(guestAddr64);
return true;
}
static void writeGuestKernelWord(uint8_t *rdram, uint32_t guestAddr, uint32_t value)
{
if (!rdram)
{
return;
}
if (uint8_t *ptr = getMemPtr(rdram, guestAddr))
{
std::memcpy(ptr, &value, sizeof(value));
}
}
static void seedGuestSyscallTableProbeLocked(uint8_t *rdram)
{
writeGuestKernelWord(rdram, kGuestSyscallTableProbeBase + 0u, kGuestSyscallTableGuestBase >> 16);
writeGuestKernelWord(rdram, kGuestSyscallTableProbeBase + 8u, kGuestSyscallTableGuestBase & 0xFFFFu);
g_syscall_mirror_addrs.insert(kGuestSyscallTableProbeBase + 0u);
g_syscall_mirror_addrs.insert(kGuestSyscallTableProbeBase + 8u);
}
static void mirrorGuestSyscallEntryLocked(uint8_t *rdram, uint32_t syscallIndex, uint32_t handler)
{
uint32_t guestAddr = 0u;
if (!tryResolveGuestSyscallMirrorAddr(syscallIndex, guestAddr))
{
return;
}
writeGuestKernelWord(rdram, guestAddr, handler);
if (handler == 0u)
{
g_syscall_mirror_addrs.erase(guestAddr);
return;
}
g_syscall_mirror_addrs.insert(guestAddr);
}
void initializeGuestKernelState(uint8_t *rdram)
{
if (!rdram)
{
return;
}
std::lock_guard<std::mutex> lock(g_syscall_override_mutex);
for (uint32_t guestAddr : g_syscall_mirror_addrs)
{
writeGuestKernelWord(rdram, guestAddr, 0u);
}
g_syscall_mirror_addrs.clear();
seedGuestSyscallTableProbeLocked(rdram);
for (const auto &entry : g_syscall_overrides)
{
mirrorGuestSyscallEntryLocked(rdram, entry.first, entry.second);
}
}
void SetSyscall(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
(void)runtime;
const uint32_t syscallIndex = getRegU32(ctx, 4);
const uint32_t handler = getRegU32(ctx, 5);
{
std::lock_guard<std::mutex> lock(g_syscall_override_mutex);
if (handler == 0u)
{
g_syscall_overrides.erase(syscallIndex);
}
else
{
g_syscall_overrides[syscallIndex] = handler;
}
mirrorGuestSyscallEntryLocked(rdram, syscallIndex, handler);
}
setReturnS32(ctx, 0);
}
// 0x3C SetupThread
// args: $a0 = gp, $a1 = stack, $a2 = stack_size, $a3 = args, $t0 = root_func
void SetupThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
@@ -364,6 +597,153 @@ static inline uint32_t normalizeKernelAlias(uint32_t addr)
return addr;
}
static uint32_t computeBuiltinFindAddressResult(uint8_t *rdram,
uint32_t originalStart,
uint32_t originalEnd,
uint32_t target)
{
uint32_t start = (originalStart + 3u) & ~0x3u;
uint32_t end = originalEnd & ~0x3u;
if (start >= end)
{
return 0u;
}
const uint32_t targetNorm = normalizeKernelAlias(target);
for (uint32_t addr = start; addr < end; addr += sizeof(uint32_t))
{
const uint8_t *entryPtr = getConstMemPtr(rdram, addr);
if (!entryPtr)
{
break;
}
uint32_t entry = 0u;
std::memcpy(&entry, entryPtr, sizeof(entry));
if (entry == target || normalizeKernelAlias(entry) == targetNorm)
{
return addr;
}
}
return 0u;
}
struct FindAddressWordSample
{
uint32_t addr = 0u;
uint32_t value = 0u;
};
struct FindAddressMatchSample
{
uint32_t addr = 0u;
uint32_t value = 0u;
bool aliasOnly = false;
};
static void logFindAddressDiagnostics(uint32_t callerPc,
uint32_t originalStart,
uint32_t originalEnd,
uint32_t alignedStart,
uint32_t alignedEnd,
uint32_t target,
uint32_t targetNorm,
bool found,
uint32_t resultAddr,
uint32_t scannedWords,
bool allZero,
bool aborted,
uint32_t abortedAddr,
const FindAddressWordSample *firstWords,
uint32_t firstWordCount,
const FindAddressWordSample *nonZeroWords,
uint32_t nonZeroWordCount,
const FindAddressMatchSample *matches,
uint32_t matchCount)
{
static std::atomic<uint32_t> s_findAddressHitLogs{0u};
static std::atomic<uint32_t> s_findAddressMissLogs{0u};
constexpr uint32_t kMaxFindAddressHitLogs = 16u;
constexpr uint32_t kMaxFindAddressMissLogs = 128u;
std::atomic<uint32_t> &counter = found ? s_findAddressHitLogs : s_findAddressMissLogs;
const uint32_t logIndex = counter.fetch_add(1u, std::memory_order_relaxed);
const uint32_t logLimit = found ? kMaxFindAddressHitLogs : kMaxFindAddressMissLogs;
if (logIndex >= logLimit)
{
return;
}
std::cerr << "[FindAddress:" << (found ? "hit" : "miss") << "]"
<< " pc=0x" << std::hex << callerPc
<< " start=0x" << originalStart
<< " end=0x" << originalEnd
<< " alignedStart=0x" << alignedStart
<< " alignedEnd=0x" << alignedEnd
<< " target=0x" << target
<< " targetNorm=0x" << targetNorm
<< " result=0x" << resultAddr
<< std::dec
<< " scannedWords=" << scannedWords
<< " allZero=" << (allZero ? "true" : "false")
<< " aborted=" << (aborted ? "true" : "false");
if (aborted)
{
std::cerr << " abortedAddr=0x" << std::hex << abortedAddr << std::dec;
}
std::cerr << std::endl;
std::cerr << " firstWords:";
if (firstWordCount == 0u)
{
std::cerr << " none";
}
else
{
for (uint32_t i = 0; i < firstWordCount; ++i)
{
std::cerr << " [0x" << std::hex << firstWords[i].addr
<< "]=0x" << firstWords[i].value;
}
std::cerr << std::dec;
}
std::cerr << std::endl;
std::cerr << " nonZeroSample:";
if (nonZeroWordCount == 0u)
{
std::cerr << " none";
}
else
{
for (uint32_t i = 0; i < nonZeroWordCount; ++i)
{
std::cerr << " [0x" << std::hex << nonZeroWords[i].addr
<< "]=0x" << nonZeroWords[i].value;
}
std::cerr << std::dec;
}
std::cerr << std::endl;
std::cerr << " matches:";
if (matchCount == 0u)
{
std::cerr << " none";
}
else
{
for (uint32_t i = 0; i < matchCount; ++i)
{
std::cerr << " [0x" << std::hex << matches[i].addr
<< "]=0x" << matches[i].value
<< (matches[i].aliasOnly ? "(alias)" : "(exact)");
}
std::cerr << std::dec;
}
std::cerr << std::endl;
}
// 0x83 FindAddress:
// - a0: table start (inclusive)
// - a1: table end (exclusive)
@@ -373,10 +753,17 @@ void FindAddress(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
(void)runtime;
uint32_t start = getRegU32(ctx, 4);
uint32_t end = getRegU32(ctx, 5);
constexpr uint32_t kFindAddressWordSamples = 8u;
constexpr uint32_t kFindAddressMatchSamples = 4u;
const uint32_t originalStart = getRegU32(ctx, 4);
const uint32_t originalEnd = getRegU32(ctx, 5);
const uint32_t target = getRegU32(ctx, 6);
const uint32_t targetNorm = normalizeKernelAlias(target);
const uint32_t callerPc = ctx->pc;
uint32_t start = originalStart;
uint32_t end = originalEnd;
// Word-scan semantics: align the search window to uint32 boundaries.
start = (start + 3u) & ~0x3u;
@@ -384,28 +771,107 @@ void FindAddress(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
if (start >= end)
{
logFindAddressDiagnostics(callerPc,
originalStart,
originalEnd,
start,
end,
target,
targetNorm,
false,
0u,
0u,
true,
false,
0u,
nullptr,
0u,
nullptr,
0u,
nullptr,
0u);
setReturnU32(ctx, 0u);
return;
}
FindAddressWordSample firstWords[kFindAddressWordSamples]{};
FindAddressWordSample nonZeroWords[kFindAddressWordSamples]{};
FindAddressMatchSample matches[kFindAddressMatchSamples]{};
uint32_t firstWordCount = 0u;
uint32_t nonZeroWordCount = 0u;
uint32_t matchCount = 0u;
uint32_t scannedWords = 0u;
uint32_t resultAddr = 0u;
uint32_t abortedAddr = 0u;
bool aborted = false;
bool allZero = true;
bool foundMatch = false;
for (uint32_t addr = start; addr < end; addr += sizeof(uint32_t))
{
const uint8_t *entryPtr = getConstMemPtr(rdram, addr);
if (!entryPtr)
{
aborted = true;
abortedAddr = addr;
break;
}
uint32_t entry = 0;
std::memcpy(&entry, entryPtr, sizeof(entry));
if (entry == target || normalizeKernelAlias(entry) == targetNorm)
++scannedWords;
if (firstWordCount < kFindAddressWordSamples)
{
setReturnU32(ctx, addr);
return;
firstWords[firstWordCount++] = {addr, entry};
}
if (entry != 0u)
{
allZero = false;
if (nonZeroWordCount < kFindAddressWordSamples)
{
nonZeroWords[nonZeroWordCount++] = {addr, entry};
}
}
const bool exactMatch = (entry == target);
const bool aliasMatch = !exactMatch && (normalizeKernelAlias(entry) == targetNorm);
if (exactMatch || aliasMatch)
{
if (!foundMatch)
{
resultAddr = addr;
foundMatch = true;
}
if (matchCount < kFindAddressMatchSamples)
{
matches[matchCount++] = {addr, entry, aliasMatch};
}
}
}
setReturnU32(ctx, 0u);
logFindAddressDiagnostics(callerPc,
originalStart,
originalEnd,
start,
end,
target,
targetNorm,
foundMatch,
resultAddr,
scannedWords,
allZero,
aborted,
abortedAddr,
firstWords,
firstWordCount,
nonZeroWords,
nonZeroWordCount,
matches,
matchCount);
setReturnU32(ctx, resultAddr);
}
void Deci2Call(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
+86
View File
@@ -757,6 +757,35 @@ void register_code_generator_tests()
t.IsTrue(generated.find("if (ctx->pc != 0xA008u) { return; }") != std::string::npos, "JAL should check return PC");
});
tc.Run("trailing JAL without decoded delay slot still emits call flow", [](TestCase &t) {
Function func;
func.name = "jal_truncated";
func.start = 0xA100;
func.end = 0xA108;
func.isRecompiled = true;
func.isStub = false;
Symbol targetSym;
targetSym.name = "some_func";
targetSym.address = 0xB000;
targetSym.isFunction = true;
Instruction jal = makeJal(0xA100, 0xB000);
CodeGenerator gen({targetSym}, {});
std::string generated = gen.generateFunction(func, {jal}, false);
printGeneratedCode("trailing JAL without decoded delay slot still emits call flow", generated);
t.IsTrue(generated.find("SET_GPR_U32(ctx, 31, 0xA108u);") != std::string::npos,
"truncated trailing JAL should still set RA");
t.IsTrue(generated.find("some_func(rdram, ctx, runtime);") != std::string::npos,
"truncated trailing JAL should still emit the call");
t.IsTrue(generated.find("if (ctx->pc != 0xA108u) { return; }") != std::string::npos,
"truncated trailing JAL should still enforce fallthrough");
t.IsTrue(generated.find("// JAL 0xB000 - Handled by branch logic") == std::string::npos,
"truncated trailing JAL must not degrade to comment-only output");
});
tc.Run("JAL to internal target becomes goto", [](TestCase &t) {
Function func;
func.name = "jal_internal";
@@ -900,6 +929,34 @@ void register_code_generator_tests()
t.IsTrue(generated.find("case 0x1308u: goto label_1308;") != std::string::npos, "switch should include return address from internal JAL");
});
tc.Run("trailing JR $31 without decoded delay slot still emits return flow", [](TestCase &t) {
Function func;
func.name = "jr_ra_truncated";
func.start = 0x1500;
func.end = 0x1540;
func.isRecompiled = true;
func.isStub = false;
Instruction jal = makeJal(0x1500, 0x1510);
Instruction jalDelay = makeNop(0x1504);
Instruction atReturn = makeNop(0x1508);
Instruction atTarget = makeNop(0x1510);
Instruction jr = makeJr(0x1514, 31);
CodeGenerator gen({}, {});
std::string generated = gen.generateFunction(func, {jal, jalDelay, atReturn, atTarget, jr}, false);
printGeneratedCode("trailing JR $31 without decoded delay slot still emits return flow", generated);
t.IsTrue(generated.find("uint32_t jumpTarget = GPR_U32(ctx, 31);") != std::string::npos,
"truncated trailing JR should still read the return target");
t.IsTrue(generated.find("switch (jumpTarget)") != std::string::npos,
"truncated trailing JR should still emit the return-target switch");
t.IsTrue(generated.find("case 0x1508u: goto label_1508;") != std::string::npos,
"truncated trailing JR should still include internal return targets");
t.IsTrue(generated.find("// JR $31 - Handled by branch logic") == std::string::npos,
"truncated trailing JR must not degrade to comment-only output");
});
tc.Run("JR non-RA emits switch for in-function jump targets", [](TestCase &t) {
Function func;
func.name = "jr_non_ra_switch";
@@ -1100,6 +1157,35 @@ void register_code_generator_tests()
"jalr fallback should not dispatch directly to tail-jump delay slot");
});
tc.Run("VU random helpers emit line comments on separate lines", [](TestCase &t) {
CodeGenerator gen({}, {});
Instruction inst{};
inst.rd = 7;
inst.vectorInfo.fsf = 2;
std::string vrnext = gen.translateVU_VRNEXT(inst);
printGeneratedCode("VU random helpers emit line comments on separate lines - VRNEXT", vrnext);
t.IsTrue(vrnext.find("// Simple LFSR-based random number generation (PS2-like behavior)\n"
" uint32_t feedback") != std::string::npos,
"VRNEXT should place the generated line comment on its own line");
std::string vrinit = gen.translateVU_VRINIT(inst);
printGeneratedCode("VU random helpers emit line comments on separate lines - VRINIT", vrinit);
t.IsTrue(vrinit.find("// PS2 uses a specific LFSR initialization pattern\n"
" if (seed == 0) seed = 1;") != std::string::npos,
"VRINIT should place the generated line comment on its own line");
std::string vrxor = gen.translateVU_VRXOR(inst);
printGeneratedCode("VU random helpers emit line comments on separate lines - VRXOR", vrxor);
t.IsTrue(vrxor.find("// XOR the current random value with the data from the VU vector register\n"
" __m128i xored") != std::string::npos,
"VRXOR should keep the XOR comment on its own line");
t.IsTrue(vrxor.find("// Apply a simple mixing function similar to PS2's LFSR\n"
" __m128i mixed") != std::string::npos,
"VRXOR should keep the LFSR comment on its own line");
});
tc.Run("resolveStubTarget allows leading underscore alias", [](TestCase &t) {
t.Equals(PS2Recompiler::resolveStubTarget("_rand"), StubTarget::Stub,
"_rand should resolve via rand stub alias");
+37
View File
@@ -311,6 +311,43 @@ void register_ps2_recompiler_tests()
}
});
tc.Run("same-function JAL targets get entry wrappers but J targets stay labels", [](TestCase &t) {
std::vector<Section> sections = {
{".text", 0x1000u, 0x40u, 0u, true, false, false, true, nullptr}
};
std::vector<Function> functions = {
makeFunction("container", 0x1000u, 0x101Cu)
};
std::unordered_map<uint32_t, std::vector<Instruction>> decodedFunctions;
decodedFunctions[0x1000u] = {
makeAbsJump(0x1000u, 0x100Cu, OPCODE_JAL),
makeNopLike(0x1004u),
makeAbsJump(0x1008u, 0x1014u, OPCODE_J),
makeNopLike(0x100Cu),
makeNopLike(0x1010u),
makeNopLike(0x1014u),
makeJrRa(0x1018u)
};
size_t discovered = PS2Recompiler::DiscoverAdditionalEntryPoints(
functions, decodedFunctions, sections);
t.Equals(discovered, static_cast<size_t>(1),
"same-function JAL should create one entry while plain J stays internal");
const bool hasCallEntry = std::any_of(
functions.begin(), functions.end(),
[](const Function &fn) { return fn.start == 0x100Cu; });
const bool hasJumpEntry = std::any_of(
functions.begin(), functions.end(),
[](const Function &fn) { return fn.start == 0x1014u && fn.name.rfind("entry_", 0) == 0; });
t.IsTrue(hasCallEntry, "same-function JAL target should be promoted to an entry wrapper");
t.IsFalse(hasJumpEntry, "same-function J target should remain an internal label only");
});
tc.Run("entry reslice handles entries without containing function", [](TestCase &t) {
std::vector<Function> functions = {
makeFunction("entry_1008", 0x1008u, 0x1018u),
+289 -1
View File
@@ -1,5 +1,6 @@
#include "MiniTest.h"
#include "ps2_runtime.h"
#include "ps2_runtime_macros.h"
#include "ps2_syscalls.h"
#include <array>
@@ -56,7 +57,7 @@ namespace
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
{
ctx.r[reg] = _mm_set_epi64x(0, static_cast<int64_t>(value));
SET_GPR_U32(&ctx, reg, value);
}
int32_t getRegS32(const R5900Context &ctx, int reg)
@@ -82,6 +83,61 @@ namespace
return dispatchNumericSyscall(syscallNumber, rdram, ctx, runtime);
}
void overrideReturnHandler(uint8_t *, R5900Context *ctx, PS2Runtime *)
{
setReturnU32(ctx, ::getRegU32(ctx, 4) + ::getRegU32(ctx, 5));
ctx->pc = ::getRegU32(ctx, 31);
}
void overrideBrokenHandler(uint8_t *, R5900Context *ctx, PS2Runtime *)
{
setReturnU32(ctx, 0xDEADBEEFu);
ctx->pc = 0x12345678u;
}
void overrideRecursiveFindAddressHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
runtime->handleSyscall(rdram, ctx, 0x83u);
ctx->pc = ::getRegU32(ctx, 31);
}
void overrideKsegCompareHandler(uint8_t *, R5900Context *ctx, PS2Runtime *)
{
auto getLowU64 = [](const R5900Context *cpu, int reg) -> uint64_t
{
return (reg == 0) ? 0u : static_cast<uint64_t>(_mm_extract_epi64(cpu->r[reg], 0));
};
auto setLowS32 = [](R5900Context *cpu, int reg, uint32_t value)
{
SET_GPR_S32(cpu, reg, value);
};
auto setLowU64 = [](R5900Context *cpu, int reg, uint64_t value)
{
SET_GPR_U64(cpu, reg, value);
};
const uint32_t nextA0 = static_cast<uint32_t>(::getRegU32(ctx, 4) + 4u);
setLowS32(ctx, 4, nextA0);
setLowU64(ctx, 2, (getLowU64(ctx, 4) < getLowU64(ctx, 5)) ? 1u : 0u);
if (getLowU64(ctx, 2) == 0u)
{
ctx->r[4] = _mm_setzero_si128();
}
setLowU64(ctx, 2, getLowU64(ctx, 4));
ctx->pc = ::getRegU32(ctx, 31);
}
constexpr uint64_t K_EXPECTED_UPPER64 = 0x1122334455667788ull;
void overridePreserveUpper64Handler(uint8_t *, R5900Context *ctx, PS2Runtime *)
{
const uint64_t hi = static_cast<uint64_t>(_mm_extract_epi64(ctx->r[4], 1));
const uint64_t low = static_cast<uint64_t>(_mm_extract_epi64(ctx->r[4], 0));
const uint64_t expectedLow = static_cast<uint64_t>(static_cast<int64_t>(static_cast<int32_t>(0x80000000u)));
setReturnU32(ctx, (hi == K_EXPECTED_UPPER64 && low == expectedLow) ? 1u : 0u);
ctx->pc = ::getRegU32(ctx, 31);
}
struct TestEnv
{
std::vector<uint8_t> rdram;
@@ -453,5 +509,237 @@ void register_ps2_runtime_kernel_tests()
0u,
"FindAddress should return 0 when no matching word exists");
});
tc.Run("SetSyscall mirrors guest kernel table entries into low memory", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
initializeGuestKernelState(env.rdram.data());
constexpr uint32_t kGuestSyscallTableGuestBase = 0x80011F80u;
constexpr uint32_t kSyscallIndex = 0x83u;
constexpr uint32_t kHandler = 0x00383548u;
constexpr uint32_t kExpectedGuestAddr = kGuestSyscallTableGuestBase + (kSyscallIndex * 4u);
constexpr uint32_t kExpectedPhysAddr = kExpectedGuestAddr & 0x1FFFFFFFu;
setRegU32(env.ctx, 4, kSyscallIndex);
setRegU32(env.ctx, 5, kHandler);
t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime),
"SetSyscall syscall should dispatch");
uint32_t mirrored = 0u;
std::memcpy(&mirrored, env.rdram.data() + kExpectedPhysAddr, sizeof(mirrored));
t.Equals(mirrored,
kHandler,
"SetSyscall should mirror handler pointers into the guest kernel syscall table");
setRegU32(env.ctx, 4, 0x80000000u);
setRegU32(env.ctx, 5, 0x80080000u);
setRegU32(env.ctx, 6, kHandler);
t.IsTrue(callSyscall(0x83u, env.rdram.data(), &env.ctx, &env.runtime),
"FindAddress syscall should dispatch");
t.Equals(static_cast<uint32_t>(getRegS32(env.ctx, 2)),
kExpectedGuestAddr,
"FindAddress should discover mirrored SetSyscall entries in low guest memory");
notifyRuntimeStop();
});
tc.Run("SetSyscall honors signed kernel-table offsets", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
initializeGuestKernelState(env.rdram.data());
constexpr uint32_t kPatchIndex = 0xFFFFC402u;
constexpr uint32_t kHandler = 0xDEADBEEFu;
constexpr uint32_t kExpectedGuestAddr = 0x80002F88u;
constexpr uint32_t kExpectedPhysAddr = kExpectedGuestAddr & 0x1FFFFFFFu;
setRegU32(env.ctx, 4, kPatchIndex);
setRegU32(env.ctx, 5, kHandler);
t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime),
"SetSyscall syscall should dispatch for signed offsets");
uint32_t mirrored = 0u;
std::memcpy(&mirrored, env.rdram.data() + kExpectedPhysAddr, sizeof(mirrored));
t.Equals(mirrored,
kHandler,
"SetSyscall should treat the syscall index as a signed offset from the kernel table base");
notifyRuntimeStop();
});
tc.Run("guest kernel syscall mirror resets between runs", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
initializeGuestKernelState(env.rdram.data());
constexpr uint32_t kGuestSyscallTableGuestBase = 0x80011F80u;
constexpr uint32_t kGuestSyscallTableProbeBase = 0x000002F0u;
constexpr uint32_t kSyscallIndex = 0x5Au;
constexpr uint32_t kHandler = 0x00383510u;
constexpr uint32_t kEntryPhysAddr = (kGuestSyscallTableGuestBase + (kSyscallIndex * 4u)) & 0x1FFFFFFFu;
setRegU32(env.ctx, 4, kSyscallIndex);
setRegU32(env.ctx, 5, kHandler);
t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime),
"SetSyscall syscall should dispatch");
notifyRuntimeStop();
initializeGuestKernelState(env.rdram.data());
uint32_t mirrored = 1u;
std::memcpy(&mirrored, env.rdram.data() + kEntryPhysAddr, sizeof(mirrored));
t.Equals(mirrored,
0u,
"Initializing guest kernel state should clear stale mirrored syscall entries");
uint32_t probeHi = 0u;
uint32_t probeLo = 0u;
std::memcpy(&probeHi, env.rdram.data() + kGuestSyscallTableProbeBase + 0u, sizeof(probeHi));
std::memcpy(&probeLo, env.rdram.data() + kGuestSyscallTableProbeBase + 8u, sizeof(probeLo));
t.Equals(probeHi,
kGuestSyscallTableGuestBase >> 16,
"Guest kernel initialization should seed the syscall table probe high word");
t.Equals(probeLo,
kGuestSyscallTableGuestBase & 0xFFFFu,
"Guest kernel initialization should seed the syscall table probe low word");
});
tc.Run("SetSyscall override dispatches guest handlers that return through the sentinel", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
constexpr uint32_t kSyscallIndex = 0x91u;
constexpr uint32_t kHandler = 0x00200000u;
env.runtime.registerFunction(kHandler, overrideReturnHandler);
setRegU32(env.ctx, 4, kSyscallIndex);
setRegU32(env.ctx, 5, kHandler);
t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime),
"SetSyscall syscall should dispatch");
setRegU32(env.ctx, 4, 7u);
setRegU32(env.ctx, 5, 5u);
t.IsTrue(callSyscall(kSyscallIndex, env.rdram.data(), &env.ctx, &env.runtime),
"Overridden syscall should dispatch through guest handler");
t.Equals(static_cast<uint32_t>(getRegS32(env.ctx, 2)),
12u,
"Successful override dispatch should propagate guest handler return value");
notifyRuntimeStop();
});
tc.Run("SetSyscall override preserves KSEG argument sign extension", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
constexpr uint32_t kSyscallIndex = 0x92u;
constexpr uint32_t kHandler = 0x00200030u;
env.runtime.registerFunction(kHandler, overrideKsegCompareHandler);
setRegU32(env.ctx, 4, kSyscallIndex);
setRegU32(env.ctx, 5, kHandler);
t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime),
"SetSyscall syscall should dispatch");
setRegU32(env.ctx, 4, 0x80000000u);
setRegU32(env.ctx, 5, 0x80080000u);
t.IsTrue(callSyscall(kSyscallIndex, env.rdram.data(), &env.ctx, &env.runtime),
"Override syscall should invoke the guest handler");
t.Equals(static_cast<uint32_t>(getRegS32(env.ctx, 2)),
0x80000004u,
"Override invocation should preserve KSEG ordering after 32-bit guest writes");
notifyRuntimeStop();
});
tc.Run("SetSyscall override preserves upper 64 bits when writing 32-bit args", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
constexpr uint32_t kSyscallIndex = 0x93u;
constexpr uint32_t kHandler = 0x00200040u;
env.runtime.registerFunction(kHandler, overridePreserveUpper64Handler);
setRegU32(env.ctx, 4, kSyscallIndex);
setRegU32(env.ctx, 5, kHandler);
t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime),
"SetSyscall syscall should dispatch");
env.ctx.r[4] = _mm_set_epi64x(static_cast<int64_t>(K_EXPECTED_UPPER64),
static_cast<int64_t>(static_cast<int32_t>(0x80000000u)));
t.IsTrue(callSyscall(kSyscallIndex, env.rdram.data(), &env.ctx, &env.runtime),
"Override syscall should invoke the guest handler");
t.Equals(static_cast<uint32_t>(getRegS32(env.ctx, 2)),
1u,
"Override invocation should preserve the upper 64 bits of 128-bit GPRs when setting 32-bit args");
notifyRuntimeStop();
});
tc.Run("broken syscall overrides fall back to builtin handlers", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
constexpr uint32_t kHandler = 0x00200010u;
constexpr uint32_t kTableBase = 0x00002000u;
constexpr uint32_t kValues[] = {
0x11111111u,
0x11223344u,
0x55555555u
};
env.runtime.registerFunction(kHandler, overrideBrokenHandler);
setRegU32(env.ctx, 4, 0x83u);
setRegU32(env.ctx, 5, kHandler);
t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime),
"SetSyscall syscall should dispatch");
writeGuestWords(env.rdram.data(), kTableBase, kValues, std::size(kValues));
setRegU32(env.ctx, 4, kTableBase);
setRegU32(env.ctx, 5, kTableBase + static_cast<uint32_t>(sizeof(kValues)));
setRegU32(env.ctx, 6, 0x11223344u);
t.IsTrue(callSyscall(0x83u, env.rdram.data(), &env.ctx, &env.runtime),
"Builtin syscall should still dispatch when override exits abnormally");
t.Equals(static_cast<uint32_t>(getRegS32(env.ctx, 2)),
kTableBase + 4u,
"Abnormal override exits should fall back to the builtin syscall implementation");
notifyRuntimeStop();
});
tc.Run("reentrant syscall overrides fall back to builtin handlers", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
constexpr uint32_t kHandler = 0x00200020u;
constexpr uint32_t kTableBase = 0x00003000u;
constexpr uint32_t kValues[] = {
0xCAFEBABEu,
0x11223344u,
0x55667788u
};
env.runtime.registerFunction(kHandler, overrideRecursiveFindAddressHandler);
setRegU32(env.ctx, 4, 0x83u);
setRegU32(env.ctx, 5, kHandler);
t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime),
"SetSyscall syscall should dispatch");
writeGuestWords(env.rdram.data(), kTableBase, kValues, std::size(kValues));
setRegU32(env.ctx, 4, kTableBase);
setRegU32(env.ctx, 5, kTableBase + static_cast<uint32_t>(sizeof(kValues)));
setRegU32(env.ctx, 6, 0x11223344u);
t.IsTrue(callSyscall(0x83u, env.rdram.data(), &env.ctx, &env.runtime),
"Reentrant override should resolve through builtin fallback");
t.Equals(static_cast<uint32_t>(getRegS32(env.ctx, 2)),
kTableBase + 4u,
"Reentrant override dispatch should use builtin syscall implementation");
notifyRuntimeStop();
});
});
}
+121
View File
@@ -1,5 +1,6 @@
#include "MiniTest.h"
#include "ps2_runtime.h"
#include "ps2_syscalls.h"
#include "ps2_stubs.h"
#include <array>
@@ -7,6 +8,11 @@
#include <cstring>
#include <vector>
namespace ps2_stubs
{
void resetSifState();
}
namespace
{
struct TestEnv
@@ -17,6 +23,7 @@ namespace
TestEnv() : rdram(PS2_RAM_SIZE, 0u)
{
ps2_stubs::resetSifState();
std::memset(&ctx, 0, sizeof(ctx));
}
};
@@ -71,6 +78,23 @@ namespace
std::memcpy(&value, rdram + addr, sizeof(value));
return value;
}
uint32_t g_dmacHandlerWriteAddr = 0u;
uint32_t g_dmacHandlerValue = 0u;
uint32_t g_dmacHandlerLastCause = 0u;
uint32_t g_dmacHandlerLastArg = 0u;
void testDmacHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
(void)runtime;
g_dmacHandlerLastCause = ::getRegU32(ctx, 4);
g_dmacHandlerLastArg = ::getRegU32(ctx, 5);
if (g_dmacHandlerWriteAddr != 0u)
{
writeGuestU32(rdram, g_dmacHandlerWriteAddr, g_dmacHandlerValue);
}
ctx->pc = 0u;
}
}
void register_ps2_sif_dma_tests()
@@ -114,6 +138,103 @@ void register_ps2_sif_dma_tests()
t.IsTrue(getRegS32(env.ctx, 2) < 0, "sceSifDmaStat should be negative when transfer is complete");
});
tc.Run("sceSifSetDma dispatches enabled DMAC handlers for cause 5", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kDescAddr = 0x00020300u;
constexpr uint32_t kSrcAddr = 0x00020400u;
constexpr uint32_t kDstAddr = 0x00020500u;
constexpr uint32_t kHandlerAddr = 0x00100000u;
constexpr uint32_t kHandlerWriteAddr = 0x00020600u;
constexpr uint32_t kHandlerArg = 0x12345678u;
g_dmacHandlerWriteAddr = kHandlerWriteAddr;
g_dmacHandlerValue = 0xCAFEBABEu;
g_dmacHandlerLastCause = 0u;
g_dmacHandlerLastArg = 0u;
env.runtime.registerFunction(kHandlerAddr, &testDmacHandler);
setRegU32(env.ctx, 4, 5u);
setRegU32(env.ctx, 5, kHandlerAddr);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kHandlerArg);
ps2_syscalls::AddDmacHandler(env.rdram.data(), &env.ctx, &env.runtime);
const int32_t handlerId = getRegS32(env.ctx, 2);
t.IsTrue(handlerId > 0, "AddDmacHandler should register a handler");
setRegU32(env.ctx, 4, 5u);
ps2_syscalls::EnableDmac(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), 0, "EnableDmac should succeed");
std::array<uint8_t, 16> payload{};
for (size_t i = 0; i < payload.size(); ++i)
{
payload[i] = static_cast<uint8_t>(0x40u + i);
}
std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size());
const Ps2SifDmaTransfer desc{
kSrcAddr,
kDstAddr,
static_cast<int32_t>(payload.size()),
0};
std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc));
setRegU32(env.ctx, 4, kDescAddr);
setRegU32(env.ctx, 5, 1u);
ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime);
t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should still report success");
t.Equals(readGuestU32(env.rdram.data(), kHandlerWriteAddr), g_dmacHandlerValue,
"sceSifSetDma should invoke registered DMAC handlers");
t.Equals(g_dmacHandlerLastCause, 5u, "DMAC handler should observe cause 5");
t.Equals(g_dmacHandlerLastArg, kHandlerArg, "DMAC handler should receive registered argument");
});
tc.Run("resetSifState seeds boot-ready SIF registers", [](TestCase &t)
{
TestEnv env;
auto getReg = [&](uint32_t reg) -> uint32_t
{
setRegU32(env.ctx, 4, reg);
ps2_stubs::sceSifGetReg(env.rdram.data(), &env.ctx, &env.runtime);
return ::getRegU32(&env.ctx, 2);
};
t.Equals(getReg(0x4u), 0x00020000u, "SIF boot status register should expose ready bit by default");
t.Equals(getReg(0x80000000u), 0u, "SIF main-address register should default to zero");
t.Equals(getReg(0x80000001u), 0u, "SIF sub-address register should default to zero");
t.Equals(getReg(0x80000002u), 0u, "SIF mscom register should default to zero");
});
tc.Run("sceSifExitCmd restores default boot-ready SIF registers", [](TestCase &t)
{
TestEnv env;
setRegU32(env.ctx, 4, 0x4u);
setRegU32(env.ctx, 5, 0x12340000u);
ps2_stubs::sceSifSetReg(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, 0x80000002u);
setRegU32(env.ctx, 5, 0x89ABCDEFu);
ps2_stubs::sceSifSetReg(env.rdram.data(), &env.ctx, &env.runtime);
ps2_stubs::sceSifExitCmd(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), 0, "sceSifExitCmd should succeed");
auto getReg = [&](uint32_t reg) -> uint32_t
{
setRegU32(env.ctx, 4, reg);
ps2_stubs::sceSifGetReg(env.rdram.data(), &env.ctx, &env.runtime);
return ::getRegU32(&env.ctx, 2);
};
t.Equals(getReg(0x4u), 0x00020000u, "sceSifExitCmd should restore the boot-ready status bit");
t.Equals(getReg(0x80000002u), 0u, "sceSifExitCmd should clear transient mscom state");
});
tc.Run("sceSifSetDma rejects invalid descriptors without partial writes", [](TestCase &t)
{
TestEnv env;
+7
View File
@@ -1,6 +1,7 @@
#include "MiniTest.h"
#include "ps2_runtime.h"
#include "ps2_syscalls.h"
#include "ps2_stubs.h"
#include <array>
#include <cstdint>
@@ -9,6 +10,11 @@
using namespace ps2_syscalls;
namespace ps2_stubs
{
void resetSifState();
}
namespace
{
constexpr int KE_OK = 0;
@@ -82,6 +88,7 @@ namespace
TestEnv() : rdram(PS2_RAM_SIZE, 0)
{
ps2_stubs::resetSifState();
std::memset(&ctx, 0, sizeof(ctx));
}
};