mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user