mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 19:07:47 -04:00
Recognize bounded MIPS switches with scheduled table constants
This commit is contained in:
@@ -79,7 +79,10 @@ using ExactAddressMapper = uint32_t (*)(uint32_t, const PS1Executable&);
|
||||
// Recognize only the canonical bounded-switch dependency chain:
|
||||
// sltiu/beq guard -> sll index,2 -> addu table address -> lw target -> jr.
|
||||
// The table constant may use either same-register or cross-register
|
||||
// `lui source; addiu base,source,lo`. `producer_lo/producer_hi` bound both
|
||||
// `lui source; addiu base,source,lo`, either before the guard or scheduled
|
||||
// exactly as `sltiu; beq; lui (delay slot); addiu; sll` without clobbering
|
||||
// the checked index or allowing direct edges to bypass the guard.
|
||||
// `producer_lo/producer_hi` bound both
|
||||
// table storage and case code for composite images; zero/zero means the full
|
||||
// executable. Every dependency, table word, and target must pass the hard
|
||||
// safety checks; otherwise the whole table is rejected.
|
||||
|
||||
@@ -894,16 +894,36 @@ bool resolve_exact_bounded_jump_table(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exact canonical guard: sltiu; beq; nop; sll. This is the same accepted
|
||||
// suffix as the Python capture verifier and excludes unrelated bounds.
|
||||
// Exact guards: sltiu; beq; nop; sll, or sltiu; beq; lui; addiu; sll.
|
||||
// The latter schedules the table's LUI in the bounds branch's delay slot.
|
||||
// Keep both forms in parity with the Python capture verifier.
|
||||
if (sll_pc < entry + 12u) return false;
|
||||
uint32_t guard_pc = sll_pc - 8u;
|
||||
uint32_t bound_pc = sll_pc - 12u;
|
||||
auto before_scale = read(sll_pc - 4u);
|
||||
if (!before_scale.has_value()) return false;
|
||||
bool scheduled_base = *before_scale != 0u;
|
||||
if (scheduled_base) {
|
||||
// R3000 JR would see the old target register without this load delay.
|
||||
if (lw_pc != jr_pc - 8u) return false;
|
||||
if (sll_pc < entry + 16u) return false;
|
||||
guard_pc -= 4u;
|
||||
bound_pc -= 4u;
|
||||
auto upper_word = read(sll_pc - 8u);
|
||||
if (!upper_word.has_value()) return false;
|
||||
uint32_t upper_reg = (*upper_word >> 16) & 0x1Fu;
|
||||
if ((*upper_word >> 26) != 0x0Fu ||
|
||||
((*upper_word >> 21) & 0x1Fu) != 0u || upper_reg == 0u ||
|
||||
(*before_scale >> 26) != 0x09u ||
|
||||
((*before_scale >> 21) & 0x1Fu) != upper_reg ||
|
||||
((*before_scale >> 16) & 0x1Fu) != base_reg ||
|
||||
upper_reg == index_reg || base_reg == index_reg) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
auto guard_word_opt = read(guard_pc);
|
||||
auto bound_word = read(bound_pc);
|
||||
auto guard_delay = read(sll_pc - 4u);
|
||||
if (!guard_word_opt.has_value() || !bound_word.has_value() ||
|
||||
!guard_delay.has_value() || *guard_delay != 0u) {
|
||||
if (!guard_word_opt.has_value() || !bound_word.has_value()) {
|
||||
return false;
|
||||
}
|
||||
uint32_t guard_word = *guard_word_opt;
|
||||
@@ -920,20 +940,23 @@ bool resolve_exact_bounded_jump_table(
|
||||
uint32_t bound_rt = (*bound_word >> 16) & 0x1Fu;
|
||||
uint32_t count = *bound_word & 0xFFFFu;
|
||||
if (bound_op != 0x0Bu || bound_rs != index_reg ||
|
||||
bound_rt != bound_reg || count == 0u || count >= 512u ||
|
||||
bound_rt != bound_reg || bound_reg == index_reg ||
|
||||
count == 0u || count >= 512u ||
|
||||
in_delay_slot(bound_pc)) {
|
||||
return false;
|
||||
}
|
||||
uint32_t guard_target = exact_branch_target(guard_pc, guard_word);
|
||||
if ((guard_target & 3u) != 0u || guard_target < entry ||
|
||||
guard_target >= hard_cap ||
|
||||
(guard_target >= sll_pc && guard_target < jr_pc + 8u)) {
|
||||
(guard_target >= sll_pc && guard_target < jr_pc + 8u) ||
|
||||
(scheduled_base && guard_target > bound_pc && guard_target < jr_pc + 8u)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve the table-base reaching definition. Cross-register constants
|
||||
// (`lui rA; addiu rB,rA,lo`) are valid, but both definitions must be local,
|
||||
// unskippable, outside delay slots, and unclobbered before use.
|
||||
// unskippable and unclobbered before use. Only the validated scheduled
|
||||
// form permits LUI in a delay slot, exactly that of the bounds BEQ.
|
||||
uint32_t low_pc = 0, source_reg = base_reg;
|
||||
int16_t low = 0;
|
||||
uint32_t lui_pc = 0, upper = 0;
|
||||
@@ -974,7 +997,9 @@ bool resolve_exact_bounded_jump_table(
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lui_pc == 0u || in_delay_slot(lui_pc) ||
|
||||
if (scheduled_base && (lui_pc != guard_pc + 4u || low_pc != sll_pc - 4u))
|
||||
return false;
|
||||
if (lui_pc == 0u || (in_delay_slot(lui_pc) && !scheduled_base) ||
|
||||
(low_pc != 0u && in_delay_slot(low_pc)) ||
|
||||
inbound_skips(lui_pc, addu_pc) ||
|
||||
(low_pc != 0u && inbound_skips(low_pc, addu_pc)) ||
|
||||
@@ -1026,6 +1051,7 @@ bool resolve_exact_bounded_jump_table(
|
||||
uint32_t runtime_target = *target_word;
|
||||
uint32_t image_target = mapped(runtime_target);
|
||||
if (image_target < entry || image_target >= hard_cap ||
|
||||
(scheduled_base && image_target > bound_pc && image_target < jr_pc + 8u) ||
|
||||
image_target < producer_lo || image_target >= producer_hi ||
|
||||
(image_target & 3u) != 0u) {
|
||||
return false;
|
||||
@@ -1089,6 +1115,7 @@ bool resolve_exact_bounded_jump_table(
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint32_t protected_pc = scheduled_base ? bound_pc : lui_pc;
|
||||
for (uint32_t source = entry; source < hard_cap; source += 4u) {
|
||||
auto word = read(source);
|
||||
if (!word.has_value()) return false;
|
||||
@@ -1096,9 +1123,9 @@ bool resolve_exact_bounded_jump_table(
|
||||
bool direct = cf.kind == ExactCfKind::Branch ||
|
||||
cf.kind == ExactCfKind::Jump ||
|
||||
cf.kind == ExactCfKind::Jal;
|
||||
if (direct && cf.target > lui_pc && cf.target <= jr_pc &&
|
||||
(source < lui_pc || source > jr_pc) &&
|
||||
!case_reachable.count(source)) {
|
||||
if (direct && cf.target > protected_pc && cf.target <= jr_pc &&
|
||||
(source < protected_pc || source > jr_pc) &&
|
||||
(scheduled_base || !case_reachable.count(source))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,95 @@ bool resolves_ape_switch(const ApeSwitchFixture& f,
|
||||
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;
|
||||
|
||||
|
||||
@@ -1068,6 +1068,29 @@ void jump_table_producer_codegen_test() {
|
||||
unbounded_alias_generated.front().full_code.find("/* jump table") !=
|
||||
std::string::npos,
|
||||
"alias regression fixture reaches the table when ownership is absent");
|
||||
|
||||
// The scheduled variant must feed code generation too, not merely the
|
||||
// discovery report. Its table base overwrites the guard in the BEQ delay
|
||||
// slot, so the existing branch emitter must preserve the tested condition.
|
||||
write_word(exe, base + 0x500u, 0u);
|
||||
write_word(exe, base + 0x504u, 0u);
|
||||
write_word(exe, base + 0x508u, 0x2C620003u);
|
||||
write_word(exe, base + 0x50Cu, 0x1040001Cu);
|
||||
write_word(exe, base + 0x510u, 0x3C028001u);
|
||||
write_word(exe, base + 0x514u, 0x24420A00u);
|
||||
write_word(exe, base + 0x518u, 0x00031880u);
|
||||
write_word(exe, base + 0x51Cu, 0x00621821u);
|
||||
write_word(exe, base + 0x520u, 0x8C620000u);
|
||||
PSXRecomp::ControlFlowAnalyzer scheduled_analyzer(exe);
|
||||
const auto scheduled_cfg = scheduled_analyzer.analyze_function(function);
|
||||
PSXRecomp::CodeGenerator scheduled_generator(exe);
|
||||
const auto scheduled = scheduled_generator.generate_function(
|
||||
function, scheduled_cfg).full_code;
|
||||
check(scheduled.find("/* jump table") != std::string::npos,
|
||||
"codegen emits a switch with its table LUI in the bounds delay slot");
|
||||
for (uint32_t target : cases)
|
||||
check(scheduled.find(fmt::format("goto block_{:08X}", target)) != std::string::npos,
|
||||
"every scheduled case has an emitted native control-flow edge");
|
||||
}
|
||||
|
||||
void cfg_codegen_load_delay_test() {
|
||||
|
||||
@@ -223,6 +223,81 @@ def check_bounded_jump_table_discovery():
|
||||
LOAD + 0x60, 2, (LOAD, LOAD + 0x180)) == set()
|
||||
|
||||
|
||||
def check_scheduled_jump_table_discovery():
|
||||
# Independent synthetic function: a checked input in v1, the table base
|
||||
# in v0, and three cases returning distinct values. No captured game bytes.
|
||||
data = bytearray(0x1000)
|
||||
for offset, word in (
|
||||
(0x508, 0x2C620003), # sltiu v0,v1,3
|
||||
(0x50C, 0x1040001C), # beq v0,zero,+0x580
|
||||
(0x510, 0x3C028001), # lui v0,0x8001 (guard delay slot)
|
||||
(0x514, 0x24420A00), # addiu v0,v0,0xa00
|
||||
(0x518, 0x00031880), # sll v1,v1,2
|
||||
(0x51C, 0x00621821), # addu v1,v1,v0
|
||||
(0x520, 0x8C620000), # lw v0,0(v1)
|
||||
(0x528, 0x00400008), # jr v0
|
||||
(0x580, 0x03E00008),
|
||||
):
|
||||
put(data, offset, word)
|
||||
cases = {LOAD + off for off in (0x540, 0x550, 0x560)}
|
||||
for i, target in enumerate(sorted(cases)):
|
||||
put(data, 0xA00 + i * 4, target)
|
||||
put(data, target - LOAD, 0x24020001 + i)
|
||||
put(data, target - LOAD + 4, 0x03E00008)
|
||||
|
||||
def resolve(image, producer=None):
|
||||
return MOD._find_jump_table_targets(
|
||||
image, LOAD, len(image), LOAD + 0x500, LOAD + 0x600,
|
||||
LOAD + 0x528, 2, producer)
|
||||
|
||||
assert resolve(data) == cases
|
||||
no_load_delay = bytearray(data)
|
||||
put(no_load_delay, 0x524, 0x00400008)
|
||||
assert MOD._find_jump_table_targets(
|
||||
no_load_delay, LOAD, len(data), LOAD + 0x500, LOAD + 0x600,
|
||||
LOAD + 0x524, 2) == set()
|
||||
walk = MOD._walk_overlay_function(
|
||||
data, LOAD, len(data), LOAD + 0x500, LOAD + 0x600)
|
||||
assert walk['jump_table_targets'] == cases
|
||||
assert cases <= walk['visited']
|
||||
renamed = bytearray(data)
|
||||
put(renamed, 0x510, 0x3C088001) # lui t0
|
||||
put(renamed, 0x514, 0x25020A00) # addiu v0,t0
|
||||
assert resolve(renamed) == cases
|
||||
signed_low = bytearray(0x9000)
|
||||
signed_low[:len(data)] = data
|
||||
signed_low[0x8A00:0x8A0C] = data[0xA00:0xA0C]
|
||||
put(signed_low, 0x510, 0x3C028002)
|
||||
put(signed_low, 0x514, 0x24428A00)
|
||||
assert resolve(signed_low) == cases
|
||||
assert resolve(data, (LOAD, LOAD + 0x900)) == set()
|
||||
|
||||
def jump(offset):
|
||||
return 0x08000000 | (((LOAD + offset) >> 2) & 0x03FFFFFF)
|
||||
|
||||
for name, mutations in (
|
||||
('bound overwrites index', ((0x508, 0x2C630003), (0x50C, 0x1060001C))),
|
||||
('LUI overwrites index', ((0x510, 0x3C038001), (0x514, 0x24620A00))),
|
||||
('ADDIU overwrites index', ((0x514, 0x24430A00),)),
|
||||
('wrong constant source', ((0x514, 0x25020A00),)),
|
||||
('reserved LUI fields', ((0x510, 0x3C228001),)),
|
||||
('call delay slot', ((0x50C, jal(LOAD + 0x580)),)),
|
||||
('branch likely', ((0x50C, 0x5040001C),)),
|
||||
('guard loop clobbers condition', ((0x50C, 0x1040FFFF),)),
|
||||
('reject into constant', ((0x50C, 0x10400000),)),
|
||||
('skip bound from before', ((0x500, jump(0x510)),)),
|
||||
('skip bound from after', ((0x590, jump(0x510)),)),
|
||||
('case skips bound', ((0x540, jump(0x510)), (0x544, 0))),
|
||||
('bound in delay slot', ((0x504, jump(0x580)),)),
|
||||
('table case skips bound', ((0xA00, LOAD + 0x510),)),
|
||||
('out of host target', ((0xA04, LOAD + 0x700),)),
|
||||
):
|
||||
broken = bytearray(data)
|
||||
for offset, word in mutations:
|
||||
put(broken, offset, word)
|
||||
assert resolve(broken) == set(), name
|
||||
|
||||
|
||||
def check_composite_call_boundaries():
|
||||
data = bytearray(0x100)
|
||||
cross_target = LOAD + 0x50
|
||||
@@ -3649,6 +3724,7 @@ def main():
|
||||
|
||||
check_composite_call_boundaries()
|
||||
check_bounded_jump_table_discovery()
|
||||
check_scheduled_jump_table_discovery()
|
||||
check_static_discovery_provenance()
|
||||
check_static_dispatch_provenance()
|
||||
check_owned_direct_call_is_interior()
|
||||
|
||||
@@ -5,7 +5,7 @@ disc image at build time, so a fresh install ships with overlay coverage instead
|
||||
of relying entirely on runtime (tcc/gcc) autocompile. See `docs/AOT_OVERLAY_PLAN.md`
|
||||
for the full design, findings, and honest coverage numbers.
|
||||
|
||||
**Status: ENHANCEMENT SPIKE. Provably-correct for clean producers, not exhaustive.**
|
||||
**Status: ENHANCEMENT SPIKE. Partial static discovery; not a correctness proof.**
|
||||
`extract_generic.py` is path-parameterized and suitable for the current build
|
||||
pipeline. Disc-container discovery remains engine-specific; unsupported producers
|
||||
fail closed instead of being guessed as code.
|
||||
@@ -41,13 +41,22 @@ the named metric.
|
||||
All 22 A*.BIN overlays converge on 0x80108F9C (region 0x80108000 +3996),
|
||||
DEMO/GAME converge on 0x80106228, and OPN resolves to 0x8018A000.
|
||||
|
||||
## Correctness guarantee (why partial coverage is safe)
|
||||
## What byte guards establish
|
||||
|
||||
Every shard is content-addressed by per-function `code_crc`; the runtime loader
|
||||
only executes a shard when live RAM byte-matches (`overlay_loader.c` lazy_man_matches).
|
||||
A mis-positioned / data-as-code shard either fails audit at compile time or never
|
||||
fires at runtime → coverage loss, never incorrect execution. Whatever static
|
||||
misses, production autocompile (tcc/gcc) self-heals on first visit.
|
||||
checks live RAM against its recorded byte ranges (`overlay_loader.c`
|
||||
`lazy_man_matches`). That protects against selecting a different overlay at the
|
||||
same address. It does not prove that bytes were correctly classified as code,
|
||||
that all dependencies were guarded, or that the emitted native code is correct.
|
||||
Captured shards are investigative references, not correctness or completeness
|
||||
oracles. Production fallback can cover additional executed paths; it cannot
|
||||
establish that a static extraction is complete.
|
||||
|
||||
The shared switch recognizer accepts a bounded `sltiu; beq; lui; addiu; sll;
|
||||
addu; lw; nop; jr` pattern, including LUI in the BEQ delay slot. Register
|
||||
dependencies, producer bounds, table targets, and direct edges must pass the
|
||||
proof checks. This recovers a compiler convention across titles without game
|
||||
addresses in the recognizer. Unknown dispatch forms still require investigation.
|
||||
|
||||
## Durable runtime capture history
|
||||
|
||||
|
||||
@@ -844,6 +844,11 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int,
|
||||
sll scaled,index,2; addu address,scaled,table_base
|
||||
lw target,offset(address); [nop x 0..1]; jr target
|
||||
|
||||
Also accept ``sltiu; beq; lui; addiu; sll``: the LUI executes in the
|
||||
guard's delay slot, then ADDIU completes the base on the in-range path.
|
||||
That exact pair may not overwrite the checked index, and direct edges
|
||||
may not bypass the bound (including edges from switch cases).
|
||||
|
||||
``table_base`` must be a nearest-definition LUI, optionally followed by a
|
||||
nearest-definition ADDIU. The lower instruction may rename the
|
||||
register (``lui v0; addiu s0,v0,lo``), as used by Ape Escape. Any other
|
||||
@@ -853,7 +858,7 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int,
|
||||
"""
|
||||
lo = load_addr
|
||||
hi = load_addr + size
|
||||
if not (entry <= jr_pc < hard_cap and 0 < jr_rs < 32):
|
||||
if not (entry <= jr_pc < hard_cap and 0 < jr_rs < 31):
|
||||
return set()
|
||||
jr_word = _word_at(data, load_addr, jr_pc)
|
||||
if jr_word != ((jr_rs << 21) | 0x08):
|
||||
@@ -904,7 +909,24 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int,
|
||||
# The bound must guard this exact index on the sole fallthrough into SLL.
|
||||
sltiu_pc = sll_pc - 12
|
||||
branch_pc = sll_pc - 8
|
||||
if _word_at(data, load_addr, sll_pc - 4) != 0:
|
||||
scheduled_base = _word_at(data, load_addr, sll_pc - 4) != 0
|
||||
if scheduled_base:
|
||||
# R3000 load delay: JR must not consume the immediately preceding LW.
|
||||
if lw_pc != jr_pc - 8:
|
||||
return set()
|
||||
sltiu_pc -= 4
|
||||
branch_pc -= 4
|
||||
upper = _word_at(data, load_addr, sll_pc - 8)
|
||||
lower = _word_at(data, load_addr, sll_pc - 4)
|
||||
if (upper is None or lower is None or
|
||||
(upper >> 26) != 0x0F or ((upper >> 21) & 31) != 0 or
|
||||
(lower >> 26) != 0x09 or ((lower >> 16) & 31) != table_reg or
|
||||
((upper >> 16) & 31) == 0 or
|
||||
((upper >> 16) & 31) != ((lower >> 21) & 31) or
|
||||
index_reg in (table_reg, (upper >> 16) & 31)):
|
||||
return set()
|
||||
if sltiu_pc < entry or (sltiu_pc >= entry + 4 and
|
||||
_is_control_flow(_word_at(data, load_addr, sltiu_pc - 4))):
|
||||
return set()
|
||||
sltiu_word = _word_at(data, load_addr, sltiu_pc)
|
||||
branch_word = _word_at(data, load_addr, branch_pc)
|
||||
@@ -913,7 +935,7 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int,
|
||||
return set()
|
||||
guard_reg = (sltiu_word >> 16) & 0x1F
|
||||
table_count = sltiu_word & 0xFFFF
|
||||
if not guard_reg or not (1 <= table_count < 512):
|
||||
if not guard_reg or guard_reg == index_reg or not (1 <= table_count < 512):
|
||||
return set()
|
||||
if branch_word is None or ((branch_word >> 26) & 0x3F) != 0x04:
|
||||
return set()
|
||||
@@ -923,7 +945,8 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int,
|
||||
return set()
|
||||
reject_target = _branch_target(branch_pc, branch_word)
|
||||
if (not (entry <= reject_target < hard_cap) or
|
||||
sll_pc <= reject_target < jr_pc + 8):
|
||||
sll_pc <= reject_target < jr_pc + 8 or
|
||||
(scheduled_base and sltiu_pc < reject_target < jr_pc + 8)):
|
||||
return set()
|
||||
|
||||
def nearest_writer(reg: int, before: int):
|
||||
@@ -962,6 +985,10 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int,
|
||||
else:
|
||||
return set()
|
||||
|
||||
if scheduled_base and (constant_pc != branch_pc + 4 or
|
||||
table_def_pc != sll_pc - 4):
|
||||
return set()
|
||||
|
||||
# Calls could clobber the proven constant. Other branches could enter the
|
||||
# suffix through an unproved path. The one exact bounds branch is allowed.
|
||||
for pc in range(constant_pc + 4, sll_pc, 4):
|
||||
@@ -971,7 +998,8 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int,
|
||||
if _is_control_flow(word) and pc != branch_pc:
|
||||
return set()
|
||||
predecessor = _word_at(data, load_addr, constant_pc - 4)
|
||||
if constant_pc >= entry + 4 and _is_control_flow(predecessor):
|
||||
if (constant_pc >= entry + 4 and _is_control_flow(predecessor) and
|
||||
not scheduled_base):
|
||||
return set()
|
||||
table_base = (table_base + lw_offset) & 0xFFFFFFFF
|
||||
table_end = table_base + table_count * 4
|
||||
@@ -984,6 +1012,7 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int,
|
||||
target = _word_at(data, load_addr, table_base + index * 4)
|
||||
if (target is None or (target & 3) or
|
||||
not (entry <= target < hard_cap) or
|
||||
(scheduled_base and sltiu_pc < target < jr_pc + 8) or
|
||||
not (range_lo <= target < range_hi) or
|
||||
not _is_valid_mips_word(_word_at(data, load_addr, target))):
|
||||
return set()
|
||||
@@ -1025,15 +1054,16 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int,
|
||||
elif kind in ('jr', 'jr_ra'):
|
||||
case_reachable.add(delay)
|
||||
|
||||
protected_pc = sltiu_pc if scheduled_base else constant_pc
|
||||
for source in range(entry, hard_cap, 4):
|
||||
source_word = _word_at(data, load_addr, source)
|
||||
if source_word is None:
|
||||
return set()
|
||||
kind, target = _classify_cf(source, source_word)
|
||||
if (kind in ('branch', 'j', 'jal') and
|
||||
constant_pc < target <= jr_pc and
|
||||
not (constant_pc <= source <= jr_pc) and
|
||||
source not in case_reachable):
|
||||
protected_pc < target <= jr_pc and
|
||||
not (protected_pc <= source <= jr_pc) and
|
||||
(scheduled_base or source not in case_reachable)):
|
||||
return set()
|
||||
if proof_out is not None:
|
||||
proof_out.append({
|
||||
|
||||
Reference in New Issue
Block a user