mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 10:38:46 -04:00
d67f0f9396
A PS-X EXE header may carry KUSEG addresses (Alien Resurrection SLUS-00633, Kula World SCES-01000); the BIOS then runs the program at KUSEG PCs, so its links, EPCs and I-cache tags are KUSEG. The parser folded such a header to KSEG0, so the whole program compiled for a segment it never runs in, and the dispatch lookup matched any segment by physical address (docs/SEGMENT_AWARE_CODE.md §3.2, §5.3, §5.5). - EXE parser: keeps the header's addresses. link_segment() is the load address's segment; the load address and entry must share it and lie in the 8 MiB RAM window of KUSEG, KSEG0 or KSEG1 (KSEG1 compiles as an uncached home segment). phys_load_address()/contains_phys() do byte addressing; the BSS overlap check and the text_size bound are physical. - function_analysis: the three JAL scans take the target's top bits from the delay slot's PC instead of assuming KSEG0 (unchanged for KSEG0). - Seeds are range-checked by physical address. A seed in the link segment is an entry (0x000100E4 was silently dropped before). A seed for the same bytes in another segment asks for a segment variant (§5.4): it is listed in a WARNING, never folded into the home body. Seeds outside the image are counted. The producer_range, cross_call_allow and hosted_interior directives have no variant form: a foreign spelling is refused with the link-segment one instead of a bare "invalid" error. - Config code sites that are matched exactly (widescreen and cull sites, mod hooks, hot/data-shard funcs, the VSync-query function and its event-horizon return PCs, persisted options) and name the image's bytes in another segment are refused with the spelling that matches, since they would now match nothing. Kinds matched by physical address (byte patches, the full-word-guarded cull keep/angle/aspect-cone and signed-X-bound sites, dome call sites) reach the same bytes in any spelling and are not refused. Overlay compiles are exempt until PR E. - tools/collect_game_misses.py wrote every dirty-RAM miss as a KSEG0 seed, which a KUSEG-linked image now reports instead of compiling. It writes them in the link segment (--segment, --game-toml, or a segmented text range; it refuses to guess from a physical one) and adds the segment_misses rows at their full PC. - Dispatch source moves to game_dispatch_emitter.cpp. The lookup indexes the physical word, then requires row.addr == addr; a second row for one word is an error until variants share the table. psx_game_address_in_text stays physical, so a segment miss takes the clean-text-miss path (the previous commit records it). For a KSEG0 title the game C is byte-identical; its dispatch file changes only in psx_game_find_entry (the exact-PC compare) and its comment. Tests: - segment_aware_codegen closes link-segment, fetch-tag-segment, irq-resume-segment, resume-pc-segment, store-pc-segment, home-seed-accepted, alias-fetch-coherence and segment-miss; they stay as regression guards, and resume-pc-segment now also checks the dispatch rows' keys and resume PCs. - kuseg_dispatch_lookup: KUSEG rows, names and baked PCs; aliases miss while text stays physical; variant seeds reported; a function reached only by jal and a SaveState-style continuation are discovered in a KUSEG image (kills the two discovery scans reverted to KSEG0; the third, the invalid-JAL data heuristic, reads the same bits either way); mixed-segment headers rejected; a KSEG1 image charged per instruction; foreign config sites refused; the RAM-mirror contract as before. - kuseg_dispatch_lookup also compiles the lookup's binary-search form (a table spanning 2 MiB or more) and queries every segment's alias of each row; refuses a KSEG0 event-horizon site and KSEG0 seeds directives with their KUSEG spelling; accepts a KSEG0 signed_x_bound site and applies it. - emitter_runtime_pc_test: the dispatch rows follow runtime_pc() and keep the compile identity in func_ names (kills B's two surviving mutants: 76 and 12 failing checks); two rows on one physical word are an error. - collect_game_misses (new, tools/tests): link-segment seeds from each source, segment misses at their full PC, no guessing, no duplicates, against a fake server using the runtime's own summary row format. - reachable_discovery_test: the parser keeps KUSEG, accepts KSEG1, rejects mixed segments and non-RAM load addresses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
884 lines
44 KiB
C++
884 lines
44 KiB
C++
#include "function_analysis.h"
|
|
#include "ps1_exe_parser.h"
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
constexpr uint32_t kLoad = 0x80010000u;
|
|
int failures = 0;
|
|
|
|
#define CHECK(cond, message) do { \
|
|
if (!(cond)) { \
|
|
std::fprintf(stderr, "FAIL: %s\n", (message)); \
|
|
failures++; \
|
|
} \
|
|
} while (0)
|
|
|
|
void put32(std::vector<uint8_t>& data, size_t offset, uint32_t value) {
|
|
data[offset + 0] = static_cast<uint8_t>(value);
|
|
data[offset + 1] = static_cast<uint8_t>(value >> 8);
|
|
data[offset + 2] = static_cast<uint8_t>(value >> 16);
|
|
data[offset + 3] = static_cast<uint8_t>(value >> 24);
|
|
}
|
|
|
|
uint32_t jal(uint32_t target) {
|
|
return 0x0C000000u | ((target >> 2) & 0x03FFFFFFu);
|
|
}
|
|
|
|
std::vector<uint8_t> make_exe_buffer(uint32_t image_size,
|
|
uint32_t entry = kLoad) {
|
|
std::vector<uint8_t> data(2048u + image_size, 0);
|
|
std::memcpy(data.data(), "PS-X EXE", 8);
|
|
put32(data, 0x10, entry);
|
|
put32(data, 0x18, kLoad);
|
|
put32(data, 0x1C, image_size);
|
|
return data;
|
|
}
|
|
|
|
PSXRecomp::PS1Executable parse(std::vector<uint8_t> data) {
|
|
std::string error;
|
|
auto exe = PSXRecomp::PS1ExeParser::parse_buffer(data, error);
|
|
CHECK(exe.has_value(), error.c_str());
|
|
return exe.value();
|
|
}
|
|
|
|
std::set<uint32_t> starts(const PSXRecomp::FunctionAnalysisResult& result) {
|
|
std::set<uint32_t> out;
|
|
for (const auto& function : result.functions) out.insert(function.start_addr);
|
|
return out;
|
|
}
|
|
|
|
struct ApeSwitchFixture {
|
|
std::vector<uint8_t> image;
|
|
uint32_t entry = kLoad + 0x500u;
|
|
uint32_t jr_pc = kLoad + 0x528u;
|
|
uint32_t table = kLoad + 0xA00u;
|
|
std::vector<uint32_t> cases = {
|
|
kLoad + 0x540u, kLoad + 0x550u, kLoad + 0x560u};
|
|
};
|
|
|
|
ApeSwitchFixture make_ape_switch() {
|
|
ApeSwitchFixture f;
|
|
f.image = make_exe_buffer(0x1000u, f.entry);
|
|
const size_t text = 2048u;
|
|
put32(f.image, text + 0x500, 0x3C088001u); // lui t0,0x8001
|
|
put32(f.image, text + 0x504, 0x25100A00u); // addiu s0,t0,0x0a00
|
|
put32(f.image, text + 0x508, 0x00000000u);
|
|
put32(f.image, text + 0x50C, 0x2C620003u); // sltiu v0,v1,3
|
|
put32(f.image, text + 0x510, 0x1040001Bu); // beq v0,zero,+0x580
|
|
put32(f.image, text + 0x514, 0x00000000u);
|
|
put32(f.image, text + 0x518, 0x00031080u); // sll v0,v1,2
|
|
put32(f.image, text + 0x51C, 0x00501021u); // addu v0,v0,s0
|
|
put32(f.image, text + 0x520, 0x8C420000u); // lw v0,0(v0)
|
|
put32(f.image, text + 0x524, 0x00000000u);
|
|
put32(f.image, text + 0x528, 0x00400008u); // jr v0
|
|
put32(f.image, text + 0x52C, 0x00000000u);
|
|
for (size_t i = 0; i < f.cases.size(); i++) {
|
|
size_t off = static_cast<size_t>(f.cases[i] - kLoad);
|
|
put32(f.image, text + off, 0x24020001u + static_cast<uint32_t>(i));
|
|
put32(f.image, text + off + 4u, 0x03E00008u);
|
|
put32(f.image, text + off + 8u, 0x00000000u);
|
|
put32(f.image, text + 0xA00u + i * 4u, f.cases[i]);
|
|
}
|
|
put32(f.image, text + 0x580, 0x03E00008u);
|
|
put32(f.image, text + 0x584, 0x00000000u);
|
|
return f;
|
|
}
|
|
|
|
bool resolves_ape_switch(const ApeSwitchFixture& f,
|
|
uint32_t entry,
|
|
uint32_t producer_lo = kLoad,
|
|
uint32_t producer_hi = kLoad + 0x1000u) {
|
|
auto exe = parse(f.image);
|
|
PSXRecomp::ExactJumpTable table;
|
|
return PSXRecomp::resolve_exact_bounded_jump_table(
|
|
exe, entry, kLoad + 0x1000u, f.jr_pc, 2u, table,
|
|
nullptr, producer_lo, producer_hi);
|
|
}
|
|
|
|
ApeSwitchFixture make_scheduled_switch() {
|
|
auto f = make_ape_switch();
|
|
const size_t text = 2048;
|
|
put32(f.image, text + 0x500, 0);
|
|
put32(f.image, text + 0x504, 0);
|
|
put32(f.image, text + 0x508, 0x2C620003u); // sltiu v0,v1,3
|
|
put32(f.image, text + 0x50C, 0x1040001Cu); // beq v0,zero,+0x580
|
|
put32(f.image, text + 0x510, 0x3C028001u); // lui v0 (guard delay)
|
|
put32(f.image, text + 0x514, 0x24420A00u); // addiu v0,v0,0xa00
|
|
put32(f.image, text + 0x518, 0x00031880u); // sll v1,v1,2
|
|
put32(f.image, text + 0x51C, 0x00621821u); // addu v1,v1,v0
|
|
put32(f.image, text + 0x520, 0x8C620000u); // lw v0,0(v1)
|
|
return f;
|
|
}
|
|
|
|
void check_scheduled_switch() {
|
|
const size_t text = 2048;
|
|
auto f = make_scheduled_switch();
|
|
auto exe = parse(f.image);
|
|
PSXRecomp::ExactJumpTable table;
|
|
CHECK(PSXRecomp::resolve_exact_bounded_jump_table(
|
|
exe, f.entry, kLoad + 0x600u, f.jr_pc, 2u, table) &&
|
|
table.table_base == f.table && table.table_count == 3u,
|
|
"scheduled table constant resolves exactly");
|
|
std::set<uint32_t> targets;
|
|
for (auto target : table.targets) targets.insert(target.second);
|
|
CHECK(targets == std::set<uint32_t>(f.cases.begin(), f.cases.end()),
|
|
"scheduled table has exactly the expected case targets");
|
|
PSXRecomp::FunctionAnalyzer analyzer(exe);
|
|
auto result = analyzer.analyze_exact_entries({f.entry});
|
|
for (auto target : f.cases)
|
|
CHECK(result.exact_reachable_pcs.count(target),
|
|
"scheduled switch cases are reached from the function root");
|
|
|
|
auto renamed = f;
|
|
put32(renamed.image, text + 0x510, 0x3C088001u);
|
|
put32(renamed.image, text + 0x514, 0x25020A00u);
|
|
CHECK(resolves_ape_switch(renamed, renamed.entry),
|
|
"scheduled constant can rename registers");
|
|
auto no_load_delay = f;
|
|
no_load_delay.jr_pc = kLoad + 0x524u;
|
|
put32(no_load_delay.image, text + 0x524, 0x00400008u);
|
|
CHECK(!resolves_ape_switch(no_load_delay, f.entry),
|
|
"scheduled switch must respect the R3000 load delay");
|
|
auto signed_low = f;
|
|
signed_low.image.resize(text + 0x9000u);
|
|
put32(signed_low.image, 0x1C, 0x9000u);
|
|
put32(signed_low.image, text + 0x510, 0x3C028002u);
|
|
put32(signed_low.image, text + 0x514, 0x24428A00u);
|
|
for (size_t i = 0; i < f.cases.size(); ++i)
|
|
put32(signed_low.image, text + 0x8A00u + 4u * i, f.cases[i]);
|
|
CHECK(resolves_ape_switch(signed_low, f.entry, kLoad, kLoad + 0x9000u),
|
|
"scheduled constant sign-extends ADDIU low half");
|
|
CHECK(!resolves_ape_switch(f, f.entry, kLoad, kLoad + 0x900u),
|
|
"scheduled table must belong to the producer");
|
|
|
|
auto jump = [](uint32_t offset) {
|
|
return 0x08000000u | (((kLoad + offset) >> 2) & 0x03FFFFFFu);
|
|
};
|
|
const std::vector<std::vector<std::pair<size_t, uint32_t>>> mutations = {
|
|
{{0x508, 0x2C630003u}, {0x50C, 0x1060001Cu}}, // bound writes index
|
|
{{0x510, 0x3C038001u}, {0x514, 0x24620A00u}}, // LUI writes index
|
|
{{0x514, 0x24430A00u}}, // ADDIU writes index
|
|
{{0x514, 0x25020A00u}}, // wrong constant source
|
|
{{0x510, 0x3C228001u}}, // reserved LUI fields
|
|
{{0x50C, jump(0x580) | 0x04000000u}}, // JAL delay
|
|
{{0x50C, 0x5040001Cu}}, // BEQL
|
|
{{0x50C, 0x1040FFFFu}}, // self-loop with clobbered condition
|
|
{{0x50C, 0x10400000u}}, // reject into LUI
|
|
{{0x500, jump(0x510)}}, // earlier edge skips bound
|
|
{{0x590, jump(0x510)}}, // later edge skips bound
|
|
{{0x540, jump(0x510)}, {0x544, 0}}, // case skips bound
|
|
{{0x504, jump(0x580)}}, // bound in delay slot
|
|
{{0xA00, kLoad + 0x510u}}, // case target skips bound
|
|
{{0xA04, kLoad + 0x2000u}}, // out of host
|
|
};
|
|
for (const auto& changes : mutations) {
|
|
auto broken = f;
|
|
for (auto change : changes)
|
|
put32(broken.image, text + change.first, change.second);
|
|
CHECK(!resolves_ape_switch(broken, broken.entry),
|
|
"unsafe scheduled-switch mutation fails closed");
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
check_scheduled_switch();
|
|
auto image = make_exe_buffer(0x2000);
|
|
const size_t text = 2048;
|
|
|
|
// Root: direct call to a framed target, direct call to a frameless leaf,
|
|
// unresolved jalr, direct call beyond the eventual bound, then return.
|
|
put32(image, text + 0x00, jal(kLoad + 0x100));
|
|
put32(image, text + 0x04, 0x00000000u);
|
|
put32(image, text + 0x08, jal(kLoad + 0x300));
|
|
put32(image, text + 0x0C, 0x00000000u);
|
|
put32(image, text + 0x10, 0x0320F809u); // jalr $ra,$t9 (unresolved)
|
|
put32(image, text + 0x14, 0x00000000u);
|
|
put32(image, text + 0x18, jal(kLoad + 0x1100));
|
|
put32(image, text + 0x1C, 0x00000000u);
|
|
put32(image, text + 0x20, 0x03E00008u);
|
|
put32(image, text + 0x24, 0x00000000u);
|
|
|
|
// Callable direct target.
|
|
put32(image, text + 0x100, 0x27BDFFF0u);
|
|
put32(image, text + 0x104, 0x03E00008u);
|
|
put32(image, text + 0x108, 0x27BD0010u);
|
|
|
|
// Valid-looking callback not referenced by a direct call.
|
|
put32(image, text + 0x200, 0x27BDFFF0u);
|
|
put32(image, text + 0x204, 0x03E00008u);
|
|
put32(image, text + 0x208, 0x27BD0010u);
|
|
|
|
// A direct JAL is sufficient callable-boundary evidence even when the
|
|
// target is a tiny frameless leaf.
|
|
put32(image, text + 0x300, 0x24020001u); // addiu $v0,$zero,1
|
|
put32(image, text + 0x304, 0x03E00008u);
|
|
put32(image, text + 0x308, 0x00000000u);
|
|
|
|
// Callable target outside the verified static bound.
|
|
put32(image, text + 0x1100, 0x27BDFFF0u);
|
|
put32(image, text + 0x1104, 0x03E00008u);
|
|
put32(image, text + 0x1108, 0x27BD0010u);
|
|
|
|
auto exe = parse(image);
|
|
std::string error;
|
|
CHECK(PSXRecomp::apply_static_analysis_bound(exe, 0x1000, error),
|
|
"valid page-aligned analysis bound is accepted");
|
|
CHECK(exe.code_size() == 0x1000 && exe.code_data.size() == 0x1000,
|
|
"accepted bound truncates header and analysis payload together");
|
|
|
|
PSXRecomp::FunctionAnalyzer analyzer(exe);
|
|
const auto reachable = starts(analyzer.analyze_exact_entries({kLoad}));
|
|
CHECK(reachable == std::set<uint32_t>({kLoad, kLoad + 0x100,
|
|
kLoad + 0x300}),
|
|
"reachable analysis follows framed and frameless direct JAL targets");
|
|
CHECK(!reachable.count(kLoad + 0x200),
|
|
"unseen indirect callback fails closed");
|
|
CHECK(!reachable.count(kLoad + 0x1100),
|
|
"direct target beyond verified bound fails closed");
|
|
|
|
// A JAL-shaped word can target an embedded table whose first pointer also
|
|
// happens to decode as a legal MIPS instruction. Three adjacent local
|
|
// pointers are stronger data evidence; keep that target interpreted.
|
|
auto pointer_image = make_exe_buffer(0x1000);
|
|
put32(pointer_image, text + 0x00, jal(kLoad + 0x400));
|
|
put32(pointer_image, text + 0x04, 0x00000000u);
|
|
put32(pointer_image, text + 0x08, 0x03E00008u);
|
|
put32(pointer_image, text + 0x0C, 0x00000000u);
|
|
put32(pointer_image, text + 0x400, kLoad + 0x500);
|
|
put32(pointer_image, text + 0x404, kLoad + 0x520);
|
|
put32(pointer_image, text + 0x408, kLoad + 0x540);
|
|
auto pointer_exe = parse(pointer_image);
|
|
PSXRecomp::FunctionAnalyzer pointer_analyzer(pointer_exe);
|
|
const auto pointer_starts = starts(
|
|
pointer_analyzer.analyze_exact_entries({kLoad}));
|
|
CHECK(!pointer_starts.count(kLoad + 0x400),
|
|
"reachable analysis rejects a dense local pointer table target");
|
|
|
|
// Discover call targets in stable rounds. An early root's JAL into the
|
|
// middle of a later explicit root must remain absorbed by that host rather
|
|
// than splitting/hard-capping it before the host is walked.
|
|
auto interior_call_image = make_exe_buffer(0x1000);
|
|
put32(interior_call_image, text + 0x00, jal(kLoad + 0x108));
|
|
put32(interior_call_image, text + 0x04, 0x00000000u);
|
|
put32(interior_call_image, text + 0x08, 0x03E00008u);
|
|
put32(interior_call_image, text + 0x0C, 0x00000000u);
|
|
put32(interior_call_image, text + 0x100, 0x24020001u);
|
|
put32(interior_call_image, text + 0x104, 0x24420001u);
|
|
put32(interior_call_image, text + 0x108, 0x24420001u);
|
|
put32(interior_call_image, text + 0x10C, 0x03E00008u);
|
|
put32(interior_call_image, text + 0x110, 0x00000000u);
|
|
auto interior_call_exe = parse(interior_call_image);
|
|
PSXRecomp::FunctionAnalyzer interior_call_analyzer(interior_call_exe);
|
|
const auto interior_call_starts = starts(
|
|
interior_call_analyzer.analyze_exact_entries(
|
|
{kLoad, kLoad + 0x100}));
|
|
CHECK(interior_call_starts ==
|
|
std::set<uint32_t>({kLoad, kLoad + 0x100}),
|
|
"direct call into an owned function stays an interior alias");
|
|
|
|
// Ownership must also settle among roots discovered in the same round.
|
|
// The initial root calls both A and an address B inside A. Neither address
|
|
// is owned by the initial partition, but walking A without B as a cap
|
|
// reaches B, so B is an alias rather than a sibling function.
|
|
auto same_round_image = make_exe_buffer(0x1000);
|
|
put32(same_round_image, text + 0x00, jal(kLoad + 0x100));
|
|
put32(same_round_image, text + 0x04, 0x00000000u);
|
|
put32(same_round_image, text + 0x08, jal(kLoad + 0x110));
|
|
put32(same_round_image, text + 0x0C, 0x00000000u);
|
|
put32(same_round_image, text + 0x10, 0x03E00008u);
|
|
put32(same_round_image, text + 0x14, 0x00000000u);
|
|
put32(same_round_image, text + 0x100, jal(kLoad + 0x110));
|
|
put32(same_round_image, text + 0x104, 0x00000000u);
|
|
put32(same_round_image, text + 0x108, 0x24420001u);
|
|
put32(same_round_image, text + 0x10C, 0x24420001u);
|
|
put32(same_round_image, text + 0x110, 0x03E00008u);
|
|
put32(same_round_image, text + 0x114, 0x00000000u);
|
|
auto same_round_exe = parse(same_round_image);
|
|
PSXRecomp::FunctionAnalyzer same_round_analyzer(same_round_exe);
|
|
const auto same_round_result =
|
|
same_round_analyzer.analyze_exact_entries({kLoad});
|
|
const auto same_round_starts = starts(same_round_result);
|
|
CHECK(same_round_starts ==
|
|
std::set<uint32_t>({kLoad, kLoad + 0x100}),
|
|
"same-round call target inside a discovered host stays absorbed");
|
|
bool same_round_hosted = false;
|
|
for (const auto& absorbed : same_round_result.absorbed_entries) {
|
|
same_round_hosted |= (absorbed.addr == kLoad + 0x110 &&
|
|
absorbed.host_start == kLoad + 0x100 &&
|
|
absorbed.addr < absorbed.host_end &&
|
|
absorbed.source_addr == kLoad + 0x100 &&
|
|
!absorbed.resolved_indirect);
|
|
}
|
|
CHECK(same_round_hosted,
|
|
"every analyzer-proven absorbed target names its final CFG host");
|
|
|
|
// Resolved constant-register calls carry the same reachable provenance as
|
|
// direct JALs and can therefore become final hosted aliases too.
|
|
auto indirect_alias_image = make_exe_buffer(0x1000);
|
|
put32(indirect_alias_image, text + 0x00, jal(kLoad + 0x100));
|
|
put32(indirect_alias_image, text + 0x04, 0x00000000u);
|
|
put32(indirect_alias_image, text + 0x08, 0x03E00008u);
|
|
put32(indirect_alias_image, text + 0x0C, 0x00000000u);
|
|
put32(indirect_alias_image, text + 0x100, 0x3C088001u);
|
|
put32(indirect_alias_image, text + 0x104, 0x25080110u);
|
|
put32(indirect_alias_image, text + 0x108, 0x0100F809u);
|
|
put32(indirect_alias_image, text + 0x10C, 0x00000000u);
|
|
put32(indirect_alias_image, text + 0x110, 0x03E00008u);
|
|
put32(indirect_alias_image, text + 0x114, 0x00000000u);
|
|
auto indirect_alias_exe = parse(indirect_alias_image);
|
|
PSXRecomp::FunctionAnalyzer indirect_alias_analyzer(indirect_alias_exe);
|
|
const auto indirect_alias_result =
|
|
indirect_alias_analyzer.analyze_exact_entries({kLoad});
|
|
bool indirect_alias_hosted = false;
|
|
for (const auto& absorbed : indirect_alias_result.absorbed_entries) {
|
|
indirect_alias_hosted |= (absorbed.addr == kLoad + 0x110 &&
|
|
absorbed.host_start == kLoad + 0x100 &&
|
|
absorbed.source_addr == kLoad + 0x108 &&
|
|
absorbed.resolved_indirect);
|
|
}
|
|
CHECK(indirect_alias_hosted,
|
|
"resolved indirect call target exports final alias provenance");
|
|
|
|
// The ordering can span rounds too: B is found first, then B discovers an
|
|
// earlier A whose reachable body contains B. Once A exists, ownership must
|
|
// be recomputed without retaining B as a stale hard cap.
|
|
auto later_round_image = make_exe_buffer(0x1000);
|
|
put32(later_round_image, text + 0x00, jal(kLoad + 0x120));
|
|
put32(later_round_image, text + 0x04, 0x00000000u);
|
|
put32(later_round_image, text + 0x08, 0x03E00008u);
|
|
put32(later_round_image, text + 0x0C, 0x00000000u);
|
|
put32(later_round_image, text + 0x100, 0x10000007u); // b 0x80010120
|
|
put32(later_round_image, text + 0x104, 0x00000000u);
|
|
put32(later_round_image, text + 0x120, jal(kLoad + 0x100));
|
|
put32(later_round_image, text + 0x124, 0x00000000u);
|
|
put32(later_round_image, text + 0x128, 0x03E00008u);
|
|
put32(later_round_image, text + 0x12C, 0x00000000u);
|
|
auto later_round_exe = parse(later_round_image);
|
|
PSXRecomp::FunctionAnalyzer later_round_analyzer(later_round_exe);
|
|
const auto later_round_starts = starts(
|
|
later_round_analyzer.analyze_exact_entries({kLoad}));
|
|
CHECK(later_round_starts ==
|
|
std::set<uint32_t>({kLoad, kLoad + 0x100}),
|
|
"later-discovered host retracts its stale interior hard cap");
|
|
|
|
// R discovers A and B; A initially owns B. Walking that expanded A then
|
|
// discovers X between them. X caps A before B and does not itself reach B,
|
|
// so final ownership must reconsider B and restore it as a real root.
|
|
auto invalidated_owner_image = make_exe_buffer(0x1000);
|
|
put32(invalidated_owner_image, text + 0x00, jal(kLoad + 0x100));
|
|
put32(invalidated_owner_image, text + 0x04, 0x00000000u);
|
|
put32(invalidated_owner_image, text + 0x08, jal(kLoad + 0x140));
|
|
put32(invalidated_owner_image, text + 0x0C, 0x00000000u);
|
|
put32(invalidated_owner_image, text + 0x10, 0x03E00008u);
|
|
put32(invalidated_owner_image, text + 0x14, 0x00000000u);
|
|
put32(invalidated_owner_image, text + 0x100,
|
|
0x08000000u | (((kLoad + 0x130) >> 2) & 0x03FFFFFFu));
|
|
put32(invalidated_owner_image, text + 0x104, 0x00000000u);
|
|
put32(invalidated_owner_image, text + 0x120, 0x03E00008u); // X
|
|
put32(invalidated_owner_image, text + 0x124, 0x00000000u);
|
|
put32(invalidated_owner_image, text + 0x130, 0x24420001u);
|
|
put32(invalidated_owner_image, text + 0x134, 0x24420001u);
|
|
put32(invalidated_owner_image, text + 0x138, 0x24420001u);
|
|
put32(invalidated_owner_image, text + 0x13C, 0x24420001u);
|
|
put32(invalidated_owner_image, text + 0x140, jal(kLoad + 0x120)); // B -> X
|
|
put32(invalidated_owner_image, text + 0x144, 0x00000000u);
|
|
put32(invalidated_owner_image, text + 0x148, 0x03E00008u);
|
|
put32(invalidated_owner_image, text + 0x14C, 0x00000000u);
|
|
auto invalidated_owner_exe = parse(invalidated_owner_image);
|
|
PSXRecomp::FunctionAnalyzer invalidated_owner_analyzer(
|
|
invalidated_owner_exe);
|
|
const auto invalidated_owner_result =
|
|
invalidated_owner_analyzer.analyze_exact_entries({kLoad});
|
|
const auto invalidated_owner_starts = starts(invalidated_owner_result);
|
|
CHECK(invalidated_owner_starts.count(kLoad + 0x100) &&
|
|
invalidated_owner_starts.count(kLoad + 0x120) &&
|
|
invalidated_owner_starts.count(kLoad + 0x140),
|
|
"R/A/X/B ownership invalidation restores B as a final root");
|
|
bool stale_b_alias = false;
|
|
for (const auto& absorbed : invalidated_owner_result.absorbed_entries)
|
|
stale_b_alias |= absorbed.addr == kLoad + 0x140;
|
|
CHECK(!stale_b_alias,
|
|
"R/A/X/B final ownership does not retain stale B alias");
|
|
|
|
// Producer boundaries are hard CFG ownership boundaries. A call into the
|
|
// next producer is rejected without approval and, when approved, becomes
|
|
// its own root rather than being absorbed by fallthrough from producer A.
|
|
auto producer_owner_image = make_exe_buffer(0x1000);
|
|
put32(producer_owner_image, text + 0x00, jal(kLoad + 0x100));
|
|
put32(producer_owner_image, text + 0x04, 0x00000000u);
|
|
put32(producer_owner_image, text + 0x08, jal(kLoad + 0x108));
|
|
put32(producer_owner_image, text + 0x0C, 0x00000000u);
|
|
put32(producer_owner_image, text + 0x10, 0x03E00008u);
|
|
put32(producer_owner_image, text + 0x14, 0x00000000u);
|
|
put32(producer_owner_image, text + 0x100, 0x24420001u);
|
|
put32(producer_owner_image, text + 0x104, 0x24420001u);
|
|
put32(producer_owner_image, text + 0x108, 0x03E00008u);
|
|
put32(producer_owner_image, text + 0x10C, 0x00000000u);
|
|
auto producer_owner_exe = parse(producer_owner_image);
|
|
const std::vector<std::pair<uint32_t, uint32_t>> ownership_ranges = {
|
|
{kLoad, kLoad + 0x108}, {kLoad + 0x108, kLoad + 0x1000}};
|
|
PSXRecomp::FunctionAnalyzer rejected_owner_analyzer(producer_owner_exe);
|
|
const auto rejected_owner = rejected_owner_analyzer.analyze_exact_entries(
|
|
{kLoad}, ownership_ranges, {});
|
|
CHECK(!starts(rejected_owner).count(kLoad + 0x108) &&
|
|
rejected_owner.absorbed_entries.empty(),
|
|
"rejected cross-producer call is neither root nor alias");
|
|
PSXRecomp::FunctionAnalyzer allowed_owner_analyzer(producer_owner_exe);
|
|
const auto allowed_owner = allowed_owner_analyzer.analyze_exact_entries(
|
|
{kLoad}, ownership_ranges, {kLoad + 0x108});
|
|
CHECK(starts(allowed_owner).count(kLoad + 0x108),
|
|
"approved cross-producer call remains a separate root");
|
|
bool cross_absorbed = false;
|
|
for (const auto& absorbed : allowed_owner.absorbed_entries)
|
|
cross_absorbed |= absorbed.addr == kLoad + 0x108;
|
|
CHECK(!cross_absorbed,
|
|
"approved cross-producer target is never absorbed across boundary");
|
|
|
|
// An uncovered composite gap has no producer identity. Two gap addresses
|
|
// both returning producer -1 must not become a synthetic shared producer.
|
|
auto producer_gap_image = make_exe_buffer(0x1000);
|
|
put32(producer_gap_image, text + 0x00, 0x03E00008u);
|
|
put32(producer_gap_image, text + 0x04, 0x00000000u);
|
|
put32(producer_gap_image, text + 0x60, 0x24020001u);
|
|
put32(producer_gap_image, text + 0x64, 0x03E00008u);
|
|
put32(producer_gap_image, text + 0x68, 0x00000000u);
|
|
const auto producer_gap_exe = parse(producer_gap_image);
|
|
PSXRecomp::FunctionAnalyzer producer_gap_analyzer(producer_gap_exe);
|
|
const std::vector<std::pair<uint32_t, uint32_t>> gap_ranges = {
|
|
{kLoad, kLoad + 0x40}, {kLoad + 0x80, kLoad + 0x100}};
|
|
const auto producer_gap_result = producer_gap_analyzer.analyze_exact_entries(
|
|
{kLoad, kLoad + 0x60}, gap_ranges, {});
|
|
CHECK(starts(producer_gap_result) == std::set<uint32_t>({kLoad}) &&
|
|
!producer_gap_result.exact_reachable_pcs.count(kLoad + 0x60),
|
|
"explicit roots in uncovered producer gaps fail closed");
|
|
|
|
// R reaches S, S calls B, and B calls earlier X. X then caps R before S,
|
|
// invalidating B's only source edge. Recomputing evidence oscillates, so the
|
|
// analyzer must fail closed to explicit R instead of retaining B forever.
|
|
auto stale_source_image = make_exe_buffer(0x1000);
|
|
put32(stale_source_image, text + 0x00,
|
|
0x08000000u | (((kLoad + 0x100) >> 2) & 0x03FFFFFFu));
|
|
put32(stale_source_image, text + 0x04, 0x00000000u);
|
|
put32(stale_source_image, text + 0x80, 0x03E00008u); // X
|
|
put32(stale_source_image, text + 0x84, 0x00000000u);
|
|
put32(stale_source_image, text + 0x100, jal(kLoad + 0x140)); // S -> B
|
|
put32(stale_source_image, text + 0x104, 0x00000000u);
|
|
put32(stale_source_image, text + 0x108, 0x03E00008u);
|
|
put32(stale_source_image, text + 0x10C, 0x00000000u);
|
|
put32(stale_source_image, text + 0x140, jal(kLoad + 0x80)); // B -> X
|
|
put32(stale_source_image, text + 0x144, 0x00000000u);
|
|
put32(stale_source_image, text + 0x148, 0x03E00008u);
|
|
put32(stale_source_image, text + 0x14C, 0x00000000u);
|
|
auto stale_source_exe = parse(stale_source_image);
|
|
PSXRecomp::FunctionAnalyzer stale_source_analyzer(stale_source_exe);
|
|
const auto stale_source_result =
|
|
stale_source_analyzer.analyze_exact_entries({kLoad});
|
|
CHECK(starts(stale_source_result) == std::set<uint32_t>({kLoad}) &&
|
|
stale_source_result.absorbed_entries.empty(),
|
|
"cyclic stale call evidence fails closed to explicit roots");
|
|
|
|
// beq rN,rN is unconditional: PC+8 is not reachable and a hidden JAL
|
|
// there cannot manufacture a function or absorbed alias.
|
|
auto always_branch_image = make_exe_buffer(0x1000);
|
|
put32(always_branch_image, text + 0x00,
|
|
(0x04u << 26) | (8u << 21) | (8u << 16) | 0x1Fu); // -> +0x80
|
|
put32(always_branch_image, text + 0x04, 0x00000000u);
|
|
put32(always_branch_image, text + 0x08, jal(kLoad + 0x40));
|
|
put32(always_branch_image, text + 0x0C, 0x00000000u);
|
|
put32(always_branch_image, text + 0x40, 0x03E00008u);
|
|
put32(always_branch_image, text + 0x44, 0x00000000u);
|
|
put32(always_branch_image, text + 0x80, 0x03E00008u);
|
|
put32(always_branch_image, text + 0x84, 0x00000000u);
|
|
auto always_branch_exe = parse(always_branch_image);
|
|
PSXRecomp::FunctionAnalyzer always_branch_analyzer(always_branch_exe);
|
|
const auto always_branch_result =
|
|
always_branch_analyzer.analyze_exact_entries({kLoad});
|
|
CHECK(starts(always_branch_result) == std::set<uint32_t>({kLoad}) &&
|
|
always_branch_result.absorbed_entries.empty(),
|
|
"always-taken branch excludes unreachable fallthrough call");
|
|
|
|
// bne rN,rN is never taken: its encoded target cannot contribute a hidden
|
|
// call edge, while its ordinary delay slot and PC+8 remain reachable.
|
|
auto never_branch_image = make_exe_buffer(0x1000);
|
|
put32(never_branch_image, text + 0x00,
|
|
(0x05u << 26) | (8u << 21) | (8u << 16) | 0x1Fu); // false -> +0x80
|
|
put32(never_branch_image, text + 0x04, 0x00000000u);
|
|
put32(never_branch_image, text + 0x08, 0x03E00008u);
|
|
put32(never_branch_image, text + 0x0C, 0x00000000u);
|
|
put32(never_branch_image, text + 0x40, 0x03E00008u);
|
|
put32(never_branch_image, text + 0x44, 0x00000000u);
|
|
put32(never_branch_image, text + 0x80, jal(kLoad + 0x40));
|
|
put32(never_branch_image, text + 0x84, 0x00000000u);
|
|
put32(never_branch_image, text + 0x88, 0x03E00008u);
|
|
put32(never_branch_image, text + 0x8C, 0x00000000u);
|
|
auto never_branch_exe = parse(never_branch_image);
|
|
PSXRecomp::FunctionAnalyzer never_branch_analyzer(never_branch_exe);
|
|
const auto never_branch_result =
|
|
never_branch_analyzer.analyze_exact_entries({kLoad});
|
|
CHECK(starts(never_branch_result) == std::set<uint32_t>({kLoad}) &&
|
|
never_branch_result.absorbed_entries.empty(),
|
|
"never-taken branch excludes unreachable target call");
|
|
|
|
// bgezal $zero is the BAL alias: the target is call evidence and the
|
|
// return continuation at PC+8 remains reachable.
|
|
auto bal_image = make_exe_buffer(0x1000);
|
|
put32(bal_image, text + 0x00,
|
|
(0x01u << 26) | (0x11u << 16) | 0x1Fu); // bal +0x80
|
|
put32(bal_image, text + 0x04, 0x00000000u);
|
|
put32(bal_image, text + 0x08, jal(kLoad + 0x40));
|
|
put32(bal_image, text + 0x0C, 0x00000000u);
|
|
put32(bal_image, text + 0x10, 0x03E00008u);
|
|
put32(bal_image, text + 0x14, 0x00000000u);
|
|
put32(bal_image, text + 0x40, 0x03E00008u);
|
|
put32(bal_image, text + 0x44, 0x00000000u);
|
|
put32(bal_image, text + 0x80, 0x03E00008u);
|
|
put32(bal_image, text + 0x84, 0x00000000u);
|
|
auto bal_exe = parse(bal_image);
|
|
PSXRecomp::FunctionAnalyzer bal_analyzer(bal_exe);
|
|
const auto bal_starts = starts(bal_analyzer.analyze_exact_entries({kLoad}));
|
|
CHECK(bal_starts.count(kLoad + 0x40) && bal_starts.count(kLoad + 0x80),
|
|
"BAL preserves call target and return-continuation discovery");
|
|
|
|
// A synthetic adjacent-producer envelope is not one linked image. The
|
|
// caller supplies the cross-boundary targets that passed its independent
|
|
// callable-CFG proof; other valid-looking targets stay with the interpreter.
|
|
PSXRecomp::FunctionAnalyzer composite_analyzer(exe);
|
|
const std::vector<std::pair<uint32_t, uint32_t>> producer_ranges = {
|
|
{kLoad, kLoad + 0x80}, {kLoad + 0x80, kLoad + 0x1000}};
|
|
const auto composite = starts(composite_analyzer.analyze_exact_entries(
|
|
{kLoad}, producer_ranges, {kLoad + 0x100}));
|
|
CHECK(composite.count(kLoad) && composite.count(kLoad + 0x100),
|
|
"approved cross-producer call target is discovered");
|
|
CHECK(!composite.count(kLoad + 0x300),
|
|
"unapproved cross-producer JAL target fails closed");
|
|
|
|
// A constant-register JR out of the current function is a statically
|
|
// proven tail call. Resolve the common lui/addiu/jr sequence without an
|
|
// execution-derived seed.
|
|
auto indirect_image = make_exe_buffer(0x1000, kLoad + 0x500);
|
|
put32(indirect_image, text + 0x400, 0x24020001u);
|
|
put32(indirect_image, text + 0x404, 0x03E00008u);
|
|
put32(indirect_image, text + 0x408, 0x00000000u);
|
|
put32(indirect_image, text + 0x500, 0x3C088001u); // lui $t0,0x8001
|
|
put32(indirect_image, text + 0x504, 0x25080400u); // addiu $t0,$t0,0x400
|
|
put32(indirect_image, text + 0x508, 0x01000008u); // jr $t0
|
|
put32(indirect_image, text + 0x50C, 0x00000000u);
|
|
auto indirect_exe = parse(indirect_image);
|
|
PSXRecomp::FunctionAnalyzer indirect_analyzer(indirect_exe);
|
|
const auto indirect = starts(
|
|
indirect_analyzer.analyze_exact_entries({kLoad + 0x500}));
|
|
CHECK(indirect.count(kLoad + 0x400) && indirect.count(kLoad + 0x500),
|
|
"constant-register JR discovers its frameless tail-call target");
|
|
|
|
// A jump enters at the low-half definition and skips the preceding LUI.
|
|
// A raw backward scan would combine them; reaching-definition proof must
|
|
// reject that synthetic indirect target.
|
|
auto split_constant_image = make_exe_buffer(0x1000, kLoad + 0x500);
|
|
put32(split_constant_image, text + 0x400, 0x03E00008u);
|
|
put32(split_constant_image, text + 0x404, 0x00000000u);
|
|
put32(split_constant_image, text + 0x500,
|
|
0x08000000u | (((kLoad + 0x518) >> 2) & 0x03FFFFFFu));
|
|
put32(split_constant_image, text + 0x504, 0x00000000u);
|
|
put32(split_constant_image, text + 0x514, 0x3C088001u);
|
|
put32(split_constant_image, text + 0x518, 0x25080400u);
|
|
put32(split_constant_image, text + 0x51C, 0x01000008u);
|
|
put32(split_constant_image, text + 0x520, 0x00000000u);
|
|
auto split_constant_exe = parse(split_constant_image);
|
|
PSXRecomp::FunctionAnalyzer split_constant_analyzer(split_constant_exe);
|
|
const auto split_constant = starts(
|
|
split_constant_analyzer.analyze_exact_entries({kLoad + 0x500}));
|
|
CHECK(!split_constant.count(kLoad + 0x400),
|
|
"indirect resolver rejects a definition skipped by an inbound edge");
|
|
|
|
// The LUI executes in a JAL delay slot, then the callee may clobber t0
|
|
// before returning to the low half. It is not a reaching definition for
|
|
// the later JALR even though the raw words are adjacent.
|
|
auto call_delay_image = make_exe_buffer(0x1000, kLoad + 0x500);
|
|
put32(call_delay_image, text + 0x300, 0x03E00008u);
|
|
put32(call_delay_image, text + 0x304, 0x00000000u);
|
|
put32(call_delay_image, text + 0x400, 0x03E00008u);
|
|
put32(call_delay_image, text + 0x404, 0x00000000u);
|
|
put32(call_delay_image, text + 0x500, jal(kLoad + 0x300));
|
|
put32(call_delay_image, text + 0x504, 0x3C088001u); // call delay LUI
|
|
put32(call_delay_image, text + 0x508, 0x25080400u);
|
|
put32(call_delay_image, text + 0x50C, 0x0100F809u);
|
|
put32(call_delay_image, text + 0x510, 0x00000000u);
|
|
put32(call_delay_image, text + 0x514, 0x03E00008u);
|
|
put32(call_delay_image, text + 0x518, 0x00000000u);
|
|
auto call_delay_exe = parse(call_delay_image);
|
|
PSXRecomp::FunctionAnalyzer call_delay_analyzer(call_delay_exe);
|
|
const auto call_delay = starts(
|
|
call_delay_analyzer.analyze_exact_entries({kLoad + 0x500}));
|
|
CHECK(call_delay.count(kLoad + 0x300) &&
|
|
!call_delay.count(kLoad + 0x400),
|
|
"indirect resolver rejects a LUI in a call delay slot");
|
|
|
|
// Ape Escape's save-screen dispatcher builds its table base across
|
|
// registers (lui t0; addiu s0,t0,lo). Prove the complete bounded switch,
|
|
// then make every adversarial mutation fail closed as one whole table.
|
|
auto ape_switch = make_ape_switch();
|
|
auto ape_switch_exe = parse(ape_switch.image);
|
|
PSXRecomp::ExactJumpTable ape_table;
|
|
CHECK(PSXRecomp::resolve_exact_bounded_jump_table(
|
|
ape_switch_exe, ape_switch.entry, kLoad + 0x1000u,
|
|
ape_switch.jr_pc, 2u, ape_table) &&
|
|
ape_table.table_base == ape_switch.table &&
|
|
ape_table.table_count == ape_switch.cases.size() &&
|
|
ape_table.targets.size() == ape_switch.cases.size(),
|
|
"Ape cross-register bounded switch resolves exactly");
|
|
PSXRecomp::FunctionAnalyzer ape_switch_analyzer(ape_switch_exe);
|
|
const auto ape_result = ape_switch_analyzer.analyze_exact_entries(
|
|
{ape_switch.entry});
|
|
bool all_ape_cases_reachable = true;
|
|
for (uint32_t target : ape_switch.cases)
|
|
all_ape_cases_reachable &= ape_result.exact_reachable_pcs.count(target) != 0;
|
|
CHECK(all_ape_cases_reachable,
|
|
"exact reachable discovery follows every validated Ape case");
|
|
|
|
auto ori_switch = make_ape_switch();
|
|
put32(ori_switch.image, text + 0x504, 0x35100A00u); // ori s0,t0,0x0a00
|
|
CHECK(!resolves_ape_switch(ori_switch, ori_switch.entry),
|
|
"unproven ORI table constants fail closed");
|
|
|
|
auto no_lw_nop_switch = make_ape_switch();
|
|
no_lw_nop_switch.jr_pc = kLoad + 0x524u;
|
|
put32(no_lw_nop_switch.image, text + 0x524, 0x00400008u);
|
|
put32(no_lw_nop_switch.image, text + 0x528, 0x00000000u);
|
|
CHECK(resolves_ape_switch(no_lw_nop_switch, no_lw_nop_switch.entry),
|
|
"zero-NOP LW-to-JR schedule matches Python parity");
|
|
|
|
auto two_lw_nop_switch = make_ape_switch();
|
|
two_lw_nop_switch.jr_pc = kLoad + 0x52Cu;
|
|
put32(two_lw_nop_switch.image, text + 0x528, 0x00000000u);
|
|
put32(two_lw_nop_switch.image, text + 0x52C, 0x00400008u);
|
|
CHECK(!resolves_ape_switch(two_lw_nop_switch,
|
|
two_lw_nop_switch.entry),
|
|
"two-NOP LW-to-JR schedule fails closed");
|
|
|
|
auto wrong_jr_switch = make_ape_switch();
|
|
put32(wrong_jr_switch.image, text + 0x528,
|
|
0x00400808u); // reserved rd field makes this noncanonical JR
|
|
CHECK(!resolves_ape_switch(wrong_jr_switch, wrong_jr_switch.entry),
|
|
"jump-table proof checks the exact JR encoding");
|
|
|
|
auto noncanonical_lui_switch = make_ape_switch();
|
|
put32(noncanonical_lui_switch.image, text + 0x500,
|
|
0x3C288001u); // LUI's reserved rs field is nonzero
|
|
CHECK(!resolves_ape_switch(noncanonical_lui_switch,
|
|
noncanonical_lui_switch.entry),
|
|
"jump-table proof rejects noncanonical LUI reserved fields");
|
|
|
|
auto cross_producer_switch = make_ape_switch();
|
|
CHECK(!resolves_ape_switch(cross_producer_switch,
|
|
cross_producer_switch.entry,
|
|
kLoad + 0x400u, kLoad + 0x900u),
|
|
"producer A cannot borrow table slots from adjacent producer B");
|
|
|
|
auto producer_case_escape = make_ape_switch();
|
|
put32(producer_case_escape.image, text + 0x540,
|
|
0x08000000u | (((kLoad + 0xB20u) >> 2) & 0x03FFFFFFu));
|
|
put32(producer_case_escape.image, text + 0x544, 0x00000000u);
|
|
put32(producer_case_escape.image, text + 0xB20,
|
|
0x08000000u | (((kLoad + 0x518u) >> 2) & 0x03FFFFFFu));
|
|
put32(producer_case_escape.image, text + 0xB24, 0x00000000u);
|
|
CHECK(!resolves_ape_switch(producer_case_escape,
|
|
producer_case_escape.entry,
|
|
kLoad + 0x400u, kLoad + 0xA20u),
|
|
"case walk cannot escape through an adjacent producer");
|
|
auto cross_producer_exe = parse(cross_producer_switch.image);
|
|
PSXRecomp::FunctionAnalyzer cross_producer_analyzer(cross_producer_exe);
|
|
const auto cross_producer_result =
|
|
cross_producer_analyzer.analyze_exact_entries(
|
|
{cross_producer_switch.entry},
|
|
{{kLoad + 0x400u, kLoad + 0x900u},
|
|
{kLoad + 0x900u, kLoad + 0x1000u}});
|
|
CHECK(!cross_producer_result.exact_reachable_pcs.count(kLoad + 0x540u),
|
|
"reachable discovery rejects a cross-producer table");
|
|
CHECK(!cross_producer_result.functions.empty() &&
|
|
cross_producer_result.functions.front().producer_lo ==
|
|
kLoad + 0x400u &&
|
|
cross_producer_result.functions.front().producer_hi ==
|
|
kLoad + 0x900u,
|
|
"exact functions retain producer bounds for code generation");
|
|
|
|
auto invalid_target_switch = make_ape_switch();
|
|
put32(invalid_target_switch.image, text + 0x550, 0xFFFFFFFFu);
|
|
CHECK(!resolves_ape_switch(invalid_target_switch,
|
|
invalid_target_switch.entry),
|
|
"one invalid target instruction rejects the entire table");
|
|
|
|
auto clobbered_switch = make_ape_switch();
|
|
put32(clobbered_switch.image, text + 0x508,
|
|
0x24100000u); // addiu s0,zero,0
|
|
CHECK(!resolves_ape_switch(clobbered_switch, clobbered_switch.entry),
|
|
"table-base clobber rejects the bounded switch");
|
|
|
|
auto skipped_def_switch = make_ape_switch();
|
|
put32(skipped_def_switch.image, text + 0x700,
|
|
0x08000000u |
|
|
(((kLoad + 0x504u) >> 2) & 0x03FFFFFFu));
|
|
put32(skipped_def_switch.image, text + 0x704, 0x00000000u);
|
|
CHECK(!resolves_ape_switch(skipped_def_switch,
|
|
skipped_def_switch.entry),
|
|
"later host bytes cannot jump into and skip a protected definition");
|
|
|
|
auto bad_bound_switch = make_ape_switch();
|
|
put32(bad_bound_switch.image, text + 0x50C,
|
|
0x2C820003u); // sltiu v0,a0,3: wrong index dependency
|
|
CHECK(!resolves_ape_switch(bad_bound_switch, bad_bound_switch.entry),
|
|
"unrelated SLTIU bound cannot validate a jump table");
|
|
|
|
auto outside_target_switch = make_ape_switch();
|
|
put32(outside_target_switch.image, text + 0xA04, kLoad + 0x2000u);
|
|
CHECK(!resolves_ape_switch(outside_target_switch,
|
|
outside_target_switch.entry),
|
|
"one out-of-image table target rejects the entire table");
|
|
|
|
// A packed BIOS thunk is also a backward-scan boundary. The frameless
|
|
// leaf after its delay slot must not be swallowed into the thunk run.
|
|
auto thunk_image = make_exe_buffer(0x1000);
|
|
put32(thunk_image, text + 0x600, 0x240A00B0u);
|
|
put32(thunk_image, text + 0x604, 0x01400008u);
|
|
put32(thunk_image, text + 0x608, 0x24090008u);
|
|
put32(thunk_image, text + 0x60C, 0x00000000u);
|
|
put32(thunk_image, text + 0x610, 0x24020001u);
|
|
put32(thunk_image, text + 0x614, 0x03E00008u);
|
|
put32(thunk_image, text + 0x618, 0x00000000u);
|
|
auto thunk_exe = parse(thunk_image);
|
|
PSXRecomp::FunctionAnalyzer thunk_analyzer(thunk_exe);
|
|
const auto thunk_starts = starts(thunk_analyzer.analyze());
|
|
CHECK(thunk_starts.count(kLoad + 0x600) &&
|
|
thunk_starts.count(kLoad + 0x610),
|
|
"packed BIOS thunk bounds the following frameless leaf");
|
|
|
|
PSXRecomp::FunctionAnalyzer seeded_analyzer(exe);
|
|
const auto seeded = starts(
|
|
seeded_analyzer.analyze_exact_entries({kLoad, kLoad + 0x200}));
|
|
CHECK(seeded.count(kLoad + 0x200),
|
|
"evidence-backed explicit callback seed is compiled");
|
|
|
|
auto bad = parse(image);
|
|
const auto original_size = bad.code_data.size();
|
|
CHECK(!PSXRecomp::apply_static_analysis_bound(bad, 0x1800, error),
|
|
"non-page-aligned truncation is rejected");
|
|
CHECK(bad.code_size() == 0x2000 && bad.code_data.size() == original_size,
|
|
"invalid bound leaves parsed image unchanged");
|
|
CHECK(!PSXRecomp::apply_static_analysis_bound(bad, 0x3000, error),
|
|
"oversized noncanonical bound is rejected");
|
|
CHECK(!PSXRecomp::apply_static_analysis_bound(bad, 0, error),
|
|
"zero bound is rejected");
|
|
|
|
auto excludes_entry = parse(image);
|
|
excludes_entry.header.initial_pc = kLoad + 0x1000;
|
|
CHECK(!PSXRecomp::apply_static_analysis_bound(excludes_entry, 0x1000, error),
|
|
"bound excluding entry point is rejected");
|
|
|
|
auto rounded = parse(make_exe_buffer(0x1800));
|
|
CHECK(PSXRecomp::apply_static_analysis_bound(rounded, 0x2000, error),
|
|
"canonical generated final-page reservation remains compatible");
|
|
CHECK(rounded.code_size() == 0x1800,
|
|
"page reservation does not widen static analysis payload");
|
|
|
|
// Retail PS-X EXEs may declare BSS inside a sector-rounded payload. The
|
|
// overlap is safe only when every overlapped file byte is padding zero.
|
|
auto zero_padded_bss = make_exe_buffer(0x1000);
|
|
put32(zero_padded_bss, 0x28, kLoad + 0xDBC); // 0x244-byte overlap
|
|
put32(zero_padded_bss, 0x2C, 0x400);
|
|
std::string zero_bss_error;
|
|
const auto zero_bss_exe = PSXRecomp::PS1ExeParser::parse_buffer(
|
|
zero_padded_bss, zero_bss_error);
|
|
CHECK(zero_bss_exe.has_value(),
|
|
"zero-only BSS/payload overlap is accepted");
|
|
|
|
auto nonzero_bss = zero_padded_bss;
|
|
nonzero_bss[text + 0xE00] = 1;
|
|
std::string nonzero_bss_error;
|
|
const auto nonzero_bss_exe = PSXRecomp::PS1ExeParser::parse_buffer(
|
|
nonzero_bss, nonzero_bss_error);
|
|
CHECK(!nonzero_bss_exe.has_value(),
|
|
"non-zero BSS/code overlap remains rejected");
|
|
CHECK(nonzero_bss_error.find("overlaps non-zero code/data") !=
|
|
std::string::npos,
|
|
"non-zero overlap reports the unsafe bytes");
|
|
|
|
auto before_code_bss = zero_padded_bss;
|
|
put32(before_code_bss, 0x28, kLoad - 4);
|
|
std::string before_code_error;
|
|
CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(
|
|
before_code_bss, before_code_error).has_value(),
|
|
"BSS beginning before the payload remains rejected");
|
|
|
|
// The KSEG0 main-RAM decode window is 8 MiB: an image placed in a retail
|
|
// mirror (a mod engine copied to 0x80780000) is valid, one crossing the
|
|
// window end is not.
|
|
auto mirror_image = make_exe_buffer(0x1000);
|
|
put32(mirror_image, 0x10, 0x80780000u);
|
|
put32(mirror_image, 0x18, 0x80780000u);
|
|
std::string mirror_error;
|
|
CHECK(PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value(),
|
|
"image in the 4th RAM mirror is accepted");
|
|
put32(mirror_image, 0x10, 0x807FF800u);
|
|
put32(mirror_image, 0x18, 0x807FF800u);
|
|
CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value() &&
|
|
mirror_error.find("beyond 0x80800000") != std::string::npos,
|
|
"image crossing the RAM decode window end is rejected");
|
|
// A KUSEG header names the same RAM as KSEG0 but keeps its segment: the
|
|
// program runs at KUSEG PCs, so it compiles there
|
|
// (docs/SEGMENT_AWARE_CODE.md §5.3).
|
|
put32(mirror_image, 0x10, 0x00780000u);
|
|
put32(mirror_image, 0x18, 0x00780000u);
|
|
{
|
|
auto kuseg = PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error);
|
|
CHECK(kuseg.has_value() && kuseg->load_address() == 0x00780000u &&
|
|
kuseg->header.initial_pc == 0x00780000u &&
|
|
kuseg->link_segment() == PSXRecomp::kSegKUSEG &&
|
|
kuseg->phys_load_address() == 0x00780000u &&
|
|
kuseg->contains_phys(0x80780FFCu) &&
|
|
!kuseg->contains_phys(0x00781000u) &&
|
|
kuseg->read_word(0x00780000u).has_value() &&
|
|
!kuseg->read_word(0x80780000u).has_value(),
|
|
"a KUSEG header keeps its segment; bytes are addressed physically");
|
|
}
|
|
put32(mirror_image, 0x10, 0x007FF800u);
|
|
put32(mirror_image, 0x18, 0x007FF800u);
|
|
CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value() &&
|
|
mirror_error.find("beyond 0x00800000") != std::string::npos,
|
|
"a KUSEG image crossing its RAM window end is rejected");
|
|
// KSEG1 is accepted as an uncached home segment.
|
|
put32(mirror_image, 0x10, 0xA0010000u);
|
|
put32(mirror_image, 0x18, 0xA0010000u);
|
|
{
|
|
auto kseg1 = PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error);
|
|
CHECK(kseg1.has_value() && kseg1->link_segment() == PSXRecomp::kSegKSEG1,
|
|
"a KSEG1-linked header is accepted in its own segment");
|
|
}
|
|
// One image, one link segment.
|
|
put32(mirror_image, 0x10, 0x80010000u);
|
|
put32(mirror_image, 0x18, 0x00010000u);
|
|
CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value() &&
|
|
mirror_error.find("different segments") != std::string::npos,
|
|
"an entry in another segment than the load address is rejected");
|
|
// Outside main RAM (KSEG2, the BIOS ROM) is not a PS-X EXE target.
|
|
put32(mirror_image, 0x10, 0xBFC00000u);
|
|
put32(mirror_image, 0x18, 0xBFC00000u);
|
|
CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value() &&
|
|
mirror_error.find("Must be in main RAM") != std::string::npos,
|
|
"a load address outside the main-RAM windows is rejected");
|
|
|
|
if (failures) {
|
|
std::fprintf(stderr, "FAILED (%d)\n", failures);
|
|
return 1;
|
|
}
|
|
std::puts("ALL PASS");
|
|
return 0;
|
|
}
|