code_generator: resolve self-limited pointer tables with an unchecked index

The bounded-jump-table resolver requires the guest's own bounds check
(`sltiu/beq`) and index scaling (`sll`), because those are what prove the
table's extent. A decoder that keeps its state as a pre-scaled byte offset
and dispatches through `lui/ori base; addu base,offset; lw; jr` has neither,
so the jr fell back to a bare CPS tail-transfer and every entry it reached
that was not already a dispatch key ran on the dirty-RAM interpreter. On
Breath of Fire III that is the file decoder's state dispatch at
`0x80164AB0` (five entries; the first entry is the word right after the
table), the last boot-EXE row a play session still interpreted after the
computed-stride resolver.

resolve_self_limited_jump_table recovers the extent from the table's own
layout instead: words are taken from the base while each is a 4-aligned
in-function code address outside every delay slot, and the table ends
where its lowest target begins, since a pointer table cannot overlap the
code it points at. As with the stride resolver the emitter switches on
the actual runtime target with the CPS tail-transfer as default, so the
recovered entries only add native cases; an index past the recovered
extent still takes the fallback. Wired as the third fallback at both
code-generator sites and in the exact-entry discovery walker.

On the title's boot EXE it matches exactly one site (five entries); every
other shard is byte-identical. The synthetic test gains a second function
with this shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin
2026-09-17 15:27:43 -04:00
committed by Matt Stanley
parent 6c98726a66
commit b0e923dcd0
4 changed files with 203 additions and 2 deletions
+31
View File
@@ -76,6 +76,10 @@ struct ExactJumpTable {
// rather than a table in memory: `table_base` is then the first word of
// the unrolled run and the targets are table_base + k * stride.
uint32_t stride = 0;
// True when the targets came from resolve_self_limited_jump_table: the
// guest checks no bound, so the extent was taken from the table's own
// layout (it ends where its lowest target begins).
bool self_limited = false;
};
using ExactAddressMapper = uint32_t (*)(uint32_t, const PS1Executable&);
@@ -130,6 +134,33 @@ bool resolve_computed_stride_jump(
ExactJumpTable& table,
ExactAddressMapper runtime_to_image = nullptr);
// Recognize an in-function pointer table indexed by a value the guest never
// bounds-checks (a stored, pre-scaled state offset rather than a checked
// case number):
//
// lui T, hi ; ori|addiu T, T, lo (table base, an in-function constant)
// addu T, T, I (either operand order; I is the byte offset)
// lw R, off(T)
// [nop]
// jr R
//
// With no guard there is no count to read, so the extent comes from the
// table itself: words are taken from the base while each is a 4-aligned
// in-function code address outside every delay slot, and the table ends
// where its lowest target begins (a pointer table cannot overlap the code
// it points at). As with resolve_computed_stride_jump the caller switches
// on the actual runtime target with the CPS tail-transfer as default, so the
// recovered entries only add native cases; a value past the recovered
// extent still takes the fallback. Requires at least two entries.
bool resolve_self_limited_jump_table(
const PS1Executable& exe,
uint32_t entry,
uint32_t hard_cap,
uint32_t jr_pc,
uint32_t jr_rs,
ExactJumpTable& table,
ExactAddressMapper runtime_to_image = nullptr);
class FunctionAnalyzer {
public:
explicit FunctionAnalyzer(const PS1Executable& exe);
+16
View File
@@ -2331,6 +2331,15 @@ std::string CodeGenerator::translate_basic_block(
block.exit_instr.address, jr_rs, exact_table,
ram_to_rom);
}
// Nor a bounded table: an in-function pointer table
// indexed by a stored, unchecked offset (a decoder's
// state dispatch). Extent from the table's own layout.
if (!have_exact_table) {
have_exact_table = resolve_self_limited_jump_table(
exe_, cfg.function_start, cfg.function_end,
block.exit_instr.address, jr_rs, exact_table,
ram_to_rom);
}
uint32_t table_base = have_exact_table
? exact_table.table_base : 0u;
uint32_t table_count = have_exact_table
@@ -2357,6 +2366,10 @@ std::string CodeGenerator::translate_basic_block(
ss << config_.indent << fmt::format(
"/* computed-stride jump into unrolled run 0x{:08X} (rom 0x{:08X}), stride {}, {} entries */\n",
table_base, rom_table_base, exact_table.stride, table_count);
} else if (exact_table.self_limited) {
ss << config_.indent << fmt::format(
"/* self-limited jump table 0x{:08X} (rom 0x{:08X}), {} entries, unchecked index */\n",
table_base, rom_table_base, table_count);
} else {
ss << config_.indent << fmt::format("/* jump table 0x{:08X} (rom 0x{:08X}), {} entries */\n",
table_base, rom_table_base, table_count);
@@ -2915,6 +2928,9 @@ void CodeGenerator::scan_jr_tables(
blk.exit_instr.address, jr_r, exact_table, ram_to_rom,
cfg.producer_lo, cfg.producer_hi) &&
!resolve_computed_stride_jump(
exe_, cfg.function_start, cfg.function_end,
blk.exit_instr.address, jr_r, exact_table, ram_to_rom) &&
!resolve_self_limited_jump_table(
exe_, cfg.function_start, cfg.function_end,
blk.exit_instr.address, jr_r, exact_table, ram_to_rom)) {
continue;
+126
View File
@@ -1289,6 +1289,130 @@ bool resolve_computed_stride_jump(
return true;
}
bool resolve_self_limited_jump_table(
const PS1Executable& exe,
uint32_t entry,
uint32_t hard_cap,
uint32_t jr_pc,
uint32_t jr_rs,
ExactJumpTable& table,
ExactAddressMapper runtime_to_image) {
table = {};
if (entry >= hard_cap || jr_pc < entry || jr_pc + 8u > hard_cap ||
(entry & 3u) != 0u || (hard_cap & 3u) != 0u ||
(jr_pc & 3u) != 0u || jr_rs == 0u || jr_rs == 31u) {
return false;
}
auto read = [&](uint32_t pc) { return exe.read_word(pc); };
auto jr_word = read(jr_pc);
if (!jr_word.has_value() ||
*jr_word != ((jr_rs & 0x1Fu) << 21u | 0x08u)) {
return false;
}
auto nearest_def = [&](uint32_t reg, uint32_t use_pc,
uint32_t window) -> std::optional<uint32_t> {
if (reg == 0u) return std::nullopt;
uint32_t pc = use_pc;
for (uint32_t n = 0; n < window && pc >= entry + 4u; ++n) {
pc -= 4u;
auto w = read(pc);
if (!w.has_value()) return std::nullopt;
if (exact_classify_cf(pc, *w).kind != ExactCfKind::Normal)
return std::nullopt;
if (exact_instruction_writes_gpr(*w, reg)) return pc;
}
return std::nullopt;
};
// lw R, off(T) — the nearest definition of the jr register; allow the
// canonical single nop between the load and the jr.
auto lw_pc = nearest_def(jr_rs, jr_pc, 2u);
if (!lw_pc.has_value()) return false;
uint32_t lw = *read(*lw_pc);
if (((lw >> 26) & 0x3Fu) != 0x23u || ((lw >> 16) & 0x1Fu) != jr_rs)
return false;
uint32_t t_reg = (lw >> 21) & 0x1Fu;
int32_t lw_off = static_cast<int32_t>(static_cast<int16_t>(lw & 0xFFFFu));
if (t_reg == 0u) return false;
// addu T, A, B
auto addu_pc = nearest_def(t_reg, *lw_pc, 6u);
if (!addu_pc.has_value()) return false;
uint32_t au = *read(*addu_pc);
if (((au >> 26) & 0x3Fu) != 0u || (au & 0x3Fu) != 0x21u ||
((au >> 6) & 0x1Fu) != 0u || ((au >> 11) & 0x1Fu) != t_reg) {
return false;
}
uint32_t a = (au >> 21) & 0x1Fu, b = (au >> 16) & 0x1Fu;
if (a == 0u || b == 0u || a == b) return false;
// One operand is `lui X,hi; ori|addiu X,X,lo` — an in-function constant.
auto constant_of = [&](uint32_t reg, uint32_t use_pc) -> std::optional<uint32_t> {
auto lo_pc = nearest_def(reg, use_pc, 6u);
if (!lo_pc.has_value()) return std::nullopt;
uint32_t lo = *read(*lo_pc);
uint32_t op = (lo >> 26) & 0x3Fu;
uint32_t rs = (lo >> 21) & 0x1Fu, rt = (lo >> 16) & 0x1Fu;
if ((op != 0x0Du && op != 0x09u) || rt != reg || rs != reg)
return std::nullopt;
auto hi_pc = nearest_def(reg, *lo_pc, 6u);
if (!hi_pc.has_value()) return std::nullopt;
uint32_t hi = *read(*hi_pc);
if (((hi >> 26) & 0x3Fu) != 0x0Fu || ((hi >> 16) & 0x1Fu) != reg)
return std::nullopt;
uint32_t v = (hi & 0xFFFFu) << 16;
if (op == 0x0Du) v |= (lo & 0xFFFFu);
else v += static_cast<uint32_t>(static_cast<int32_t>(static_cast<int16_t>(lo & 0xFFFFu)));
return v;
};
std::optional<uint32_t> base = constant_of(a, *addu_pc);
if (!base.has_value()) base = constant_of(b, *addu_pc);
if (!base.has_value()) return false;
const uint32_t runtime_base = *base + static_cast<uint32_t>(lw_off);
auto mapped = [&](uint32_t addr) {
return runtime_to_image ? runtime_to_image(addr, exe) : addr;
};
const uint32_t image_base = mapped(runtime_base);
if ((image_base & 3u) != 0u || image_base < entry || image_base >= hard_cap)
return false;
auto in_delay_slot = [&](uint32_t pc) {
if (pc < entry + 4u) return false;
auto prev = read(pc - 4u);
return prev.has_value() &&
exact_classify_cf(pc - 4u, *prev).kind != ExactCfKind::Normal;
};
// Walk the table: stop at the first word that is not an in-function code
// address, or when the walk reaches the lowest target seen so far.
ExactJumpTable resolved;
resolved.table_base = runtime_base;
resolved.self_limited = true;
uint32_t lowest_target = hard_cap;
for (uint32_t i = 0; i < 256u; ++i) {
uint32_t slot = image_base + i * 4u;
if (slot >= lowest_target || slot + 4u > hard_cap) break;
auto word = read(slot);
if (!word.has_value()) break;
uint32_t rt_target = *word;
uint32_t it_target = mapped(rt_target);
if ((it_target & 3u) != 0u || it_target < entry || it_target >= hard_cap ||
it_target == jr_pc + 4u || in_delay_slot(it_target)) {
break;
}
auto first = read(it_target);
if (!first.has_value() || !exact_is_valid_mips_word(*first)) break;
// A target inside the table itself would be data run as code.
if (it_target >= image_base && it_target <= slot) break;
resolved.targets.emplace_back(rt_target, it_target);
lowest_target = std::min(lowest_target, it_target);
}
if (resolved.targets.size() < 2u) return false;
resolved.table_count = static_cast<uint32_t>(resolved.targets.size());
table = std::move(resolved);
return true;
}
FunctionAnalysisResult FunctionAnalyzer::analyze_exact_entries(
const std::vector<uint32_t>& entries,
const std::vector<std::pair<uint32_t, uint32_t>>& producer_ranges,
@@ -1462,6 +1586,8 @@ FunctionAnalysisResult FunctionAnalyzer::analyze_exact_entries(
exe_, entry, hard_cap, pc, jr_rs, table,
nullptr, producer_lo, producer_hi) ||
resolve_computed_stride_jump(
exe_, entry, hard_cap, pc, jr_rs, table) ||
resolve_self_limited_jump_table(
exe_, entry, hard_cap, pc, jr_rs, table)) {
bool all_owned = std::all_of(
table.targets.begin(), table.targets.end(),
@@ -27,6 +27,11 @@ Synthesized function (load 0x80010000):
Expected: a `computed-stride jump` switch with cases for every group and the
tail, each a `goto block_...`, and the CPS tail-transfer kept as default.
A second function (see build_exe) covers the sibling shape from the same
decoder: an in-function pointer table indexed by a stored, unchecked byte
offset (`lui/ori base; addu; lw; jr`, no sltiu/beq guard, no sll), whose
extent resolve_self_limited_jump_table takes from the table's own layout.
Usage: python test_computed_stride_jump_codegen.py [--recompiler <psxrecomp-game.exe>]
"""
import argparse
@@ -65,6 +70,22 @@ def build_exe():
for k in range(4):
body += [0x90CD0000 | k, 0x00000000, 0xA0ED0000 | k] # lbu t5,k(a2); nop; sb t5,k(a3)
body += [0x03E00008, 0x00000000] # jr ra; nop
body += [0x00000000] * 4 # pad to 0x80010060
# Second function @ 0x80010060: a self-limited pointer table indexed by a
# stored, unchecked byte offset (no sltiu/beq guard, no sll):
# lui t5,0x8001 ; ori t5,t5,0x007C ; addu t5,a0,t5 ; lw t4,0(t5) ; nop ; jr t4 ; nop (0x60..0x78)
# table @ 0x8001007C: 0x80010084, 0x80010090 (ends where its lowest target begins)
# 0x80010084: addiu v0,zero,1 ; j exit ; nop
# 0x80010090: addiu v0,zero,2 ; j exit ; nop
# 0x8001009C exit: jr ra ; nop
exit_pc = 0x8001009C
j_exit = 0x08000000 | ((exit_pc >> 2) & 0x03FFFFFF)
body += [0x3C0D8001, 0x35AD007C, 0x008D6821, 0x8DAC0000, 0x00000000, 0x01800008, 0x00000000]
body += [0x80010084, 0x80010090]
body += [0x24020001, j_exit, 0x00000000]
body += [0x24020002, j_exit, 0x00000000]
body += [0x03E00008, 0x00000000]
return make_psxexe(LOAD, w(body))
@@ -76,7 +97,7 @@ def gen_c(recompiler, tmp):
with open(psx, "wb") as f:
f.write(build_exe())
with open(seeds, "w") as f:
f.write("0x80010000\n")
f.write("0x80010000\n0x80010060\n")
r = subprocess.run([recompiler, psx, "--seeds", seeds, "--out-dir", out],
capture_output=True, text=True)
if r.returncode != 0:
@@ -120,11 +141,18 @@ def main():
if ("block_%08X:" % t) not in c:
fails.append("no interior label block_%08X" % t)
# Second function: the self-limited table.
if not re.search(r"/\* self-limited jump table 0x8001007C \(rom 0x8001007C\), 2 entries, unchecked index \*/", c):
fails.append("no self-limited table switch comment for table 0x8001007C / 2 entries")
for t in (0x80010084, 0x80010090):
if not re.search(r"case 0x%08Xu:\s*(?:\n.*?)*?goto block_%08X;" % (t, t), c):
fails.append("missing self-limited case 0x%08X -> goto block_%08X" % (t, t))
for f in fails:
print("FAIL:", f)
if fails:
return 1
print("PASS: computed-stride jump emits a switch over every unrolled-run entry.")
print("PASS: computed-stride jump and self-limited table each emit a switch over every entry.")
return 0