diff --git a/recompiler/include/function_analysis.h b/recompiler/include/function_analysis.h index 4eb5d6b8..b006c712 100644 --- a/recompiler/include/function_analysis.h +++ b/recompiler/include/function_analysis.h @@ -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); diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 1a0318e2..24e0e692 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -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; diff --git a/recompiler/src/function_analysis.cpp b/recompiler/src/function_analysis.cpp index 658868b0..5b1564db 100644 --- a/recompiler/src/function_analysis.cpp +++ b/recompiler/src/function_analysis.cpp @@ -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 { + 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(static_cast(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 { + 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(static_cast(static_cast(lo & 0xFFFFu))); + return v; + }; + std::optional 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(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(resolved.targets.size()); + table = std::move(resolved); + return true; +} + FunctionAnalysisResult FunctionAnalyzer::analyze_exact_entries( const std::vector& entries, const std::vector>& 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(), diff --git a/recompiler/tests/test_computed_stride_jump_codegen.py b/recompiler/tests/test_computed_stride_jump_codegen.py index 2bfc9a4f..c4ba5a65 100644 --- a/recompiler/tests/test_computed_stride_jump_codegen.py +++ b/recompiler/tests/test_computed_stride_jump_codegen.py @@ -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 ] """ 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