#include "function_analysis.h" #include #include #include #include #include namespace PSXRecomp { FunctionAnalyzer::FunctionAnalyzer(const PS1Executable& exe) : exe_(exe) {} void FunctionAnalyzer::add_forced_entry(uint32_t addr) { // Validate address is within the EXE range if (addr >= exe_.header.load_address && addr < exe_.analysis_end_address()) { forced_entry_points_.push_back(addr); } } bool FunctionAnalyzer::is_jr_ra(uint32_t instr) { // jr $ra: opcode=0, rs=31 ($ra), rt=0, rd=0, shamt=0, funct=8 (jr) // Format: 000000 11111 00000 00000 00000 001000 // Hex: 0x03E00008 return instr == 0x03E00008; } bool FunctionAnalyzer::is_prologue(uint32_t instr, int32_t& stack_size) { // addiu $sp, $sp, -N // Format: 001001 11101 11101 <16-bit signed immediate> // Opcode: 0x27 (addiu), rs=$sp (29), rt=$sp (29) uint32_t opcode = (instr >> 26) & 0x3F; uint32_t rs = (instr >> 21) & 0x1F; uint32_t rt = (instr >> 16) & 0x1F; int16_t imm = (int16_t)(instr & 0xFFFF); if (opcode == 0x09 && rs == 29 && rt == 29 && imm < 0) { stack_size = -imm; // Store positive stack frame size return true; } return false; } bool FunctionAnalyzer::is_epilogue(uint32_t instr, int32_t& stack_size) { // addiu $sp, $sp, +N uint32_t opcode = (instr >> 26) & 0x3F; uint32_t rs = (instr >> 21) & 0x1F; uint32_t rt = (instr >> 16) & 0x1F; int16_t imm = (int16_t)(instr & 0xFFFF); if (opcode == 0x09 && rs == 29 && rt == 29 && imm > 0) { stack_size = imm; return true; } return false; } bool FunctionAnalyzer::is_valid_mips_word(uint32_t instr) { if (instr == 0xFFFFFFFFu || instr == 0xFFFFFFFDu) return false; uint32_t opcode = (instr >> 26) & 0x3Fu; uint32_t funct = instr & 0x3Fu; uint32_t rt = (instr >> 16) & 0x1Fu; if (opcode == 0x00u) { switch (funct) { case 0x00u: case 0x02u: case 0x03u: case 0x04u: case 0x06u: case 0x07u: case 0x08u: case 0x09u: case 0x0Cu: case 0x0Du: case 0x10u: case 0x11u: case 0x12u: case 0x13u: case 0x18u: case 0x19u: case 0x1Au: case 0x1Bu: case 0x20u: case 0x21u: case 0x22u: case 0x23u: case 0x24u: case 0x25u: case 0x26u: case 0x27u: case 0x2Au: case 0x2Bu: return true; default: return false; } } if (opcode == 0x01u) { return rt == 0x00u || rt == 0x01u || rt == 0x10u || rt == 0x11u; } switch (opcode) { case 0x02u: case 0x03u: case 0x04u: case 0x05u: case 0x06u: case 0x07u: case 0x08u: case 0x09u: case 0x0Au: case 0x0Bu: case 0x0Cu: case 0x0Du: case 0x0Eu: case 0x0Fu: case 0x10u: case 0x12u: case 0x20u: case 0x21u: case 0x22u: case 0x23u: case 0x24u: case 0x25u: case 0x26u: case 0x28u: case 0x29u: case 0x2Au: case 0x2Bu: case 0x2Eu: case 0x30u: case 0x32u: case 0x38u: case 0x3Au: return true; default: return false; } } bool FunctionAnalyzer::is_branch_or_jump(uint32_t instr) { uint32_t opcode = (instr >> 26) & 0x3F; // J, JAL if (opcode == 0x02 || opcode == 0x03) return true; // BEQ, BNE, BLEZ, BGTZ if (opcode >= 0x04 && opcode <= 0x07) return true; // REGIMM: BLTZ, BGEZ, BLTZAL, BGEZAL if (opcode == 0x01) return true; // SPECIAL: JR, JALR if (opcode == 0x00) { uint32_t funct = instr & 0x3F; if (funct == 0x08 || funct == 0x09) return true; } // COP1/COP2 branches (BC1F, BC1T, BC2F, BC2T) — opcode 0x11 or 0x12, rs=0x08 if ((opcode == 0x11 || opcode == 0x12) && ((instr >> 21) & 0x1F) == 0x08) return true; return false; } static bool is_load_imm_zero_u16(uint32_t instr, uint32_t& rt_out, uint32_t& imm_out) { uint32_t opcode = (instr >> 26) & 0x3F; uint32_t rs = (instr >> 21) & 0x1F; if ((opcode != 0x09 && opcode != 0x0D) || rs != 0) { return false; } rt_out = (instr >> 16) & 0x1F; imm_out = instr & 0xFFFFu; return true; } bool FunctionAnalyzer::is_bios_dispatch_thunk(uint32_t addr, uint32_t& jr_addr_out) const { auto w0 = exe_.read_word(addr); auto w1 = exe_.read_word(addr + 4); auto w2 = exe_.read_word(addr + 8); if (!w0.has_value() || !w1.has_value() || !w2.has_value()) { return false; } uint32_t target_reg = 0; uint32_t vector = 0; if (!is_load_imm_zero_u16(*w0, target_reg, vector)) { return false; } if (vector != 0xA0u && vector != 0xB0u && vector != 0xC0u) { return false; } uint32_t op1 = (*w1 >> 26) & 0x3F; uint32_t rs1 = (*w1 >> 21) & 0x1F; uint32_t fn1 = *w1 & 0x3F; if (op1 != 0 || fn1 != 0x08u || rs1 != target_reg) { return false; } uint32_t index_reg = 0; uint32_t index = 0; if (!is_load_imm_zero_u16(*w2, index_reg, index) || index_reg != 9) { return false; } jr_addr_out = addr + 4; return true; } static bool is_sw_reg_base(uint32_t instr, uint32_t base_reg, uint32_t value_reg) { uint32_t opcode = (instr >> 26) & 0x3F; uint32_t rs = (instr >> 21) & 0x1F; uint32_t rt = (instr >> 16) & 0x1F; return opcode == 0x2B && rs == base_reg && rt == value_reg; } static bool is_sw_reg_sp(uint32_t instr, uint32_t value_reg) { return is_sw_reg_base(instr, 29, value_reg); } static bool is_lui(uint32_t instr, uint32_t& rt_out) { uint32_t opcode = (instr >> 26) & 0x3F; if (opcode != 0x0F) return false; rt_out = (instr >> 16) & 0x1F; return true; } static bool is_nop(uint32_t instr) { return instr == 0; } static uint32_t skip_leading_padding_nops(const PS1Executable& exe, uint32_t start_addr, uint32_t limit_addr) { constexpr uint32_t max_padding_bytes = 32; uint32_t addr = start_addr; uint32_t skipped = 0; while (addr + 4u <= limit_addr && skipped < max_padding_bytes) { auto word_opt = exe.read_word(addr); if (!word_opt.has_value() || !is_nop(*word_opt)) { break; } addr += 4u; skipped += 4u; } return addr; } static bool is_load_from_reg_base(uint32_t instr, uint32_t base_reg, uint32_t& rt_out) { uint32_t opcode = (instr >> 26) & 0x3F; uint32_t rs = (instr >> 21) & 0x1F; uint32_t rt = (instr >> 16) & 0x1F; switch (opcode) { case 0x20: /* lb */ case 0x21: /* lh */ case 0x22: /* lwl */ case 0x23: /* lw */ case 0x24: /* lbu */ case 0x25: /* lhu */ case 0x26: /* lwr */ break; default: return false; } if (rs != base_reg) return false; rt_out = rt; return true; } static bool is_load_imm_zero(uint32_t instr, uint32_t& rt_out) { uint32_t opcode = (instr >> 26) & 0x3F; uint32_t rs = (instr >> 21) & 0x1F; if ((opcode != 0x09 && opcode != 0x0D) || rs != 0) return false; rt_out = (instr >> 16) & 0x1F; return rt_out != 0; } static bool preprologue_window_is_valid(const PS1Executable& exe, uint32_t start_addr, uint32_t prologue_addr, uint32_t exe_start) { if (start_addr >= prologue_addr) return false; /* The candidate may start immediately after a previous return delay slot, * but it cannot itself be a branch delay slot. */ if (start_addr >= exe_start + 4) { auto prev_opt = exe.read_word(start_addr - 4); if (prev_opt.has_value() && FunctionAnalyzer::is_branch_or_jump(*prev_opt)) { return false; } } std::set global_base_regs; bool saw_global_load = false; for (uint32_t addr = start_addr; addr < prologue_addr; addr += 4) { auto word_opt = exe.read_word(addr); if (!word_opt.has_value()) return false; uint32_t instr = *word_opt; if (FunctionAnalyzer::is_branch_or_jump(instr)) return false; uint32_t rt = 0; if (is_lui(instr, rt) && rt != 0 && rt != 29 && rt != 31) { global_base_regs.insert(rt); continue; } if (is_load_imm_zero(instr, rt) && rt != 29 && rt != 31) { continue; } bool valid_load = false; for (uint32_t base_reg : global_base_regs) { uint32_t load_rt = 0; if (is_load_from_reg_base(instr, base_reg, load_rt) && load_rt != 0 && load_rt != 29 && load_rt != 31) { valid_load = true; saw_global_load = true; break; } } if (valid_load) continue; return false; } return saw_global_load; } static bool find_preprologue_setup_start(const PS1Executable& exe, uint32_t prologue_addr, uint32_t exe_start, uint32_t& setup_start_out) { constexpr uint32_t max_setup_insns = 6; bool found = false; uint32_t best_start = prologue_addr; for (uint32_t count = 1; count <= max_setup_insns; count++) { uint32_t bytes = count * 4u; if (prologue_addr < exe_start + bytes) break; uint32_t candidate = prologue_addr - bytes; if (preprologue_window_is_valid(exe, candidate, prologue_addr, exe_start)) { best_start = candidate; found = true; } } if (!found) return false; setup_start_out = best_start; return true; } static bool pointer_target_has_near_prologue(const PS1Executable& exe, uint32_t target, uint32_t exe_start, uint32_t exe_end, uint32_t& prologue_addr_out) { constexpr uint32_t max_prelude_bytes = 32; for (uint32_t addr = target; addr < exe_end && addr <= target + max_prelude_bytes; addr += 4) { auto word_opt = exe.read_word(addr); if (!word_opt.has_value()) break; int32_t stack_size = 0; if (!FunctionAnalyzer::is_prologue(*word_opt, stack_size)) continue; if (addr == target) { prologue_addr_out = addr; return true; } uint32_t setup_start = 0; if (find_preprologue_setup_start(exe, addr, exe_start, setup_start) && setup_start == target) { prologue_addr_out = addr; return true; } } return false; } uint32_t FunctionAnalyzer::find_function_start(uint32_t return_addr) { // Scan backward from jr $ra to find function start // Heuristic: Look for prologue or function alignment (16-byte boundary after prev function) uint32_t search_addr = return_addr; const uint32_t max_search = 4096; // Search up to 4096 instructions backward (16 KB) for (uint32_t i = 0; i < max_search; i++) { search_addr -= 4; if (search_addr < exe_.header.load_address) { // Reached beginning of code return exe_.header.load_address; } auto word_opt = exe_.read_word(search_addr); if (!word_opt.has_value()) { return return_addr; // Can't read, assume current position } uint32_t instr = *word_opt; int32_t stack_size; // Check if this is a prologue if (is_prologue(instr, stack_size)) { // Verify this isn't a delay slot of a branch/jump instruction. // MIPS compilers often place stack allocation in the delay slot of // the function's first conditional branch, e.g.: // beq v0, zero, skip // addiu sp, sp, -N <- delay slot, looks like prologue but isn't a function start // If the preceding instruction is a branch/jump, skip this candidate. if (search_addr >= exe_.header.load_address + 4) { auto prev_opt = exe_.read_word(search_addr - 4); if (prev_opt.has_value() && is_branch_or_jump(*prev_opt)) { continue; // delay slot, not a real prologue — keep scanning } } { uint32_t setup_start = 0; if (find_preprologue_setup_start(exe_, search_addr, exe_.header.load_address, setup_start)) { return setup_start; } } return search_addr; } // Check if we hit another function's return if (is_jr_ra(instr)) { // We've gone too far backward and hit another function // Return the address after this jr $ra (+ 8 for delay slot), then // skip alignment padding. Explicit JAL/forced entries still keep // their exact target addresses; this only normalizes starts inferred // from the previous function's trailing gap. return skip_leading_padding_nops(exe_, search_addr + 8, return_addr); } // Packed Psy-Q BIOS thunks tail-jump through A0/B0/C0 and therefore do // not contain `jr $ra`. Treat their JR delay slot as a real boundary // while scanning backward, otherwise the following frameless routine // is swallowed into the whole preceding thunk run. if (search_addr >= exe_.header.load_address + 4u) { uint32_t thunk_jr = 0; if (is_bios_dispatch_thunk(search_addr - 4u, thunk_jr) && thunk_jr == search_addr) { return skip_leading_padding_nops(exe_, search_addr + 8u, return_addr); } } } // Couldn't find clear start, assume max search distance return return_addr - (max_search * 4); } bool FunctionAnalyzer::is_likely_data_section(uint32_t start_addr, uint32_t end_addr) const { uint32_t size = end_addr - start_addr; if (size < 100) return false; // Minimum check size uint32_t total_words = size / 4; uint32_t invalid_jal_count = 0; uint32_t undefined_opcode_count = 0; // Valid PS1 (MIPS R3000) primary opcodes static const bool valid_opcode[64] = { true, true, true, true, true, true, true, true, // 0x00-0x07 true, true, true, true, true, true, true, true, // 0x08-0x0F true, false, true, false, false, false, false, false, // 0x10-0x17 (COP0=0x10, COP2=0x12) false, false, false, false, false, false, false, false, // 0x18-0x1F true, true, true, true, true, true, true, false, // 0x20-0x27 true, true, true, true, false, false, true, false, // 0x28-0x2F true, false, true, false, false, false, false, false, // 0x30-0x37 (LWC0=0x30, LWC2=0x32) true, false, true, false, false, false, false, false, // 0x38-0x3F (SWC0=0x38, SWC2=0x3A) }; for (uint32_t addr = start_addr; addr < end_addr; addr += 4) { auto word_opt = exe_.read_word(addr); if (!word_opt.has_value()) break; uint32_t instr = *word_opt; uint32_t opcode = (instr >> 26) & 0x3F; // Check for JAL with invalid target if (opcode == 3) { // JAL opcode // PS1 JAL target: upper 4 bits from the delay slot's PC (the // image's own segment), low 28 bits from instr. Valid targets lie // in the first 2 MiB of RAM. uint32_t target = ((addr + 4u) & 0xF0000000u) | ((instr & 0x03FFFFFFu) << 2); if ((target & 0x0FFFFFFFu) > 0x001FFFFFu) { invalid_jal_count++; } } // Check for undefined opcode if (!valid_opcode[opcode]) { undefined_opcode_count++; } } // Size-graduated JAL threshold: higher ratio needed for smaller functions uint32_t jal_ratio_x100 = (total_words > 0) ? (invalid_jal_count * 100u / total_words) : 0u; if (size >= 10000 && jal_ratio_x100 > 5u) return true; // Large: >5% invalid JALs if (size >= 1000 && jal_ratio_x100 > 30u) return true; // Medium: >30% invalid JALs if (size >= 100 && jal_ratio_x100 > 60u) return true; // Small: >60% invalid JALs // Undefined opcode check: // Real PS1 code consistently uses 0% undefined opcodes (calibrated on Tomba!). // Data sections masquerading as functions have ~9-27% undefined opcodes. // A safe threshold of 7% catches all observed data sections with no false positives. uint32_t undef_ratio_x100 = (total_words > 0) ? (undefined_opcode_count * 100u / total_words) : 0u; if (size >= 1000 && undef_ratio_x100 > 7u) return true; // Large: >7% undefined opcodes if (size >= 400 && undef_ratio_x100 > 50u) return true; // Small: >50% undefined opcodes (conservative) return false; } namespace { enum class ExactCfKind { Normal, Branch, BranchNever, BranchNeverLikely, Jump, Jal, JrRa, JrOther, Jalr, }; struct ExactCf { ExactCfKind kind = ExactCfKind::Normal; uint32_t target = 0; }; struct ExactWalkResult { std::set visited; std::set direct_jal_targets; // target -> reachable (source PC, was resolved jalr/jr) evidence std::map>> transfer_sources; std::set jump_table_targets; uint32_t jr_ra_count = 0; }; static ExactCf exact_classify_cf(uint32_t pc, uint32_t instr); static bool exact_is_jr_ra_word(uint32_t instr) { return instr == 0x03E00008u; } static bool exact_is_addiu_sp_neg(uint32_t instr) { uint32_t opcode = (instr >> 26) & 0x3Fu; uint32_t rs = (instr >> 21) & 0x1Fu; uint32_t rt = (instr >> 16) & 0x1Fu; int16_t imm = static_cast(instr & 0xFFFFu); return opcode == 0x09u && rs == 29u && rt == 29u && imm < 0; } static bool exact_is_valid_mips_word(uint32_t instr) { return FunctionAnalyzer::is_valid_mips_word(instr); } static uint32_t exact_branch_target(uint32_t pc, uint32_t instr) { int16_t imm = static_cast(instr & 0xFFFFu); return pc + 4u + (static_cast(imm) << 2); } static uint32_t exact_jump_target(uint32_t pc, uint32_t instr) { return ((pc + 4u) & 0xF0000000u) | ((instr & 0x03FFFFFFu) << 2); } static bool exact_instruction_writes_gpr(uint32_t instr, uint32_t reg) { if (reg == 0u) return false; uint32_t opcode = (instr >> 26) & 0x3Fu; uint32_t rs = (instr >> 21) & 0x1Fu; uint32_t rt = (instr >> 16) & 0x1Fu; uint32_t rd = (instr >> 11) & 0x1Fu; uint32_t funct = instr & 0x3Fu; if (opcode == 0x00u) { // SPECIAL instructions with a GPR destination use rd. JR, stores to // HI/LO, multiply/divide, syscall, and break have no GPR result. switch (funct) { case 0x08u: case 0x0Cu: case 0x0Du: case 0x11u: case 0x13u: case 0x18u: case 0x19u: case 0x1Au: case 0x1Bu: return false; default: return rd == reg; } } if (opcode == 0x03u) return reg == 31u; // JAL if (opcode == 0x01u && (rt == 0x10u || rt == 0x11u)) return reg == 31u; // Immediate ALU ops, LUI, and loads write rt. Branches and stores do not. if ((opcode >= 0x08u && opcode <= 0x0Fu) || (opcode >= 0x20u && opcode <= 0x26u) || opcode == 0x30u || opcode == 0x32u) { return rt == reg; } // MFC/CFC from COP0/COP2 write rt; MTC/CTC do not. if ((opcode == 0x10u || opcode == 0x12u) && (rs == 0u || rs == 2u)) { return rt == reg; } return false; } // Resolve the common absolute indirect-transfer idiom within one basic block: // // lui rN, hi(target) // addiu/ori rN, rN, lo(target) // jr/jalr rN // // The backward scan stops at control flow or any intervening write to rN, so a // stale constant from another path cannot be mistaken for the transfer target. static bool exact_resolve_constant_transfer(const PS1Executable& exe, uint32_t entry, uint32_t transfer_pc, uint32_t target_reg, uint32_t& target_out) { bool have_low = false; bool low_is_addiu = false; uint16_t low = 0; for (uint32_t count = 1; count <= 16u; count++) { uint32_t bytes = count * 4u; if (transfer_pc < entry + bytes) break; uint32_t pc = transfer_pc - bytes; auto word_opt = exe.read_word(pc); if (!word_opt.has_value()) break; uint32_t instr = *word_opt; uint32_t opcode = (instr >> 26) & 0x3Fu; uint32_t rs = (instr >> 21) & 0x1Fu; uint32_t rt = (instr >> 16) & 0x1Fu; if (!have_low && (opcode == 0x09u || opcode == 0x0Du) && rs == target_reg && rt == target_reg) { have_low = true; low_is_addiu = opcode == 0x09u; low = static_cast(instr & 0xFFFFu); continue; } if (opcode == 0x0Fu && rt == target_reg) { // A definition in a control-transfer delay slot is not a local // reaching definition for the linear suffix. A call can clobber // the register before returning at pc+4; a branch/jump can enter a // different path entirely. Without path/interprocedural proof, // reject every such candidate rather than combine raw words. if (pc >= entry + 4u) { auto predecessor = exe.read_word(pc - 4u); if (predecessor.has_value()) { uint32_t predecessor_op = (*predecessor >> 26) & 0x3Fu; if (FunctionAnalyzer::is_branch_or_jump(*predecessor) || (predecessor_op >= 0x14u && predecessor_op <= 0x17u)) { return false; } } } // A linear backward scan is only a reaching-definition proof when // no direct control-flow edge can enter the suffix after this LUI. // For example, `j low; lui rN,hi; low: addiu rN,rN,lo; jalr rN` // skips the LUI at runtime. Without this boundary check the raw // words still look like a constant pair and manufacture a target. bool crossed_inbound_boundary = false; for (uint32_t source = entry; source < transfer_pc; source += 4u) { auto source_word = exe.read_word(source); if (!source_word.has_value()) continue; ExactCf source_cf = exact_classify_cf(source, *source_word); bool has_direct_target = source_cf.kind == ExactCfKind::Branch || source_cf.kind == ExactCfKind::Jump || source_cf.kind == ExactCfKind::Jal; if (has_direct_target && source_cf.target > pc && source_cf.target <= transfer_pc) { crossed_inbound_boundary = true; break; } } if (crossed_inbound_boundary) return false; uint32_t upper = (instr & 0xFFFFu) << 16; if (!have_low) { target_out = upper; } else if (low_is_addiu) { target_out = upper + static_cast( static_cast(static_cast(low))); } else { target_out = upper | low; } return true; } if (exact_instruction_writes_gpr(instr, target_reg)) break; if (FunctionAnalyzer::is_branch_or_jump(instr)) break; } return false; } static ExactCf exact_classify_cf(uint32_t pc, uint32_t instr) { ExactCf cf; uint32_t opcode = (instr >> 26) & 0x3Fu; uint32_t funct = instr & 0x3Fu; uint32_t rs = (instr >> 21) & 0x1Fu; if (opcode == 0x00u && funct == 0x08u) { cf.kind = (rs == 31u) ? ExactCfKind::JrRa : ExactCfKind::JrOther; return cf; } if (opcode == 0x00u && funct == 0x09u) { cf.kind = ExactCfKind::Jalr; return cf; } if (opcode == 0x02u) { cf.kind = ExactCfKind::Jump; cf.target = exact_jump_target(pc, instr); return cf; } if (opcode == 0x03u) { cf.kind = ExactCfKind::Jal; cf.target = exact_jump_target(pc, instr); return cf; } uint32_t rt = (instr >> 16) & 0x1Fu; // `beq rN,rN,target` is an unconditional branch. Treating its // syntactic fallthrough as reachable lets raw data after the delay slot // manufacture call/ownership evidence. if ((opcode == 0x04u || opcode == 0x14u) && rs == rt) { cf.kind = ExactCfKind::Jump; cf.target = exact_branch_target(pc, instr); return cf; } // Conversely, `bne rN,rN,target` can never take its encoded target. Keep // its delay slot and PC+8 path, but never walk target-shaped data. if (opcode == 0x05u && rs == rt) { cf.kind = ExactCfKind::BranchNever; return cf; } // Branch-likely annuls its delay slot when the condition is false. if (opcode == 0x15u && rs == rt) { cf.kind = ExactCfKind::BranchNeverLikely; return cf; } // Conditions against the architectural zero register are also exact. if (opcode == 0x06u && rs == 0u) { // blez $zero cf.kind = ExactCfKind::Jump; cf.target = exact_branch_target(pc, instr); return cf; } if (opcode == 0x07u && rs == 0u) { // bgtz $zero cf.kind = ExactCfKind::BranchNever; return cf; } if (opcode == 0x16u && rs == 0u) { // blezl $zero cf.kind = ExactCfKind::Jump; cf.target = exact_branch_target(pc, instr); return cf; } if (opcode == 0x17u && rs == 0u) { // bgtzl $zero cf.kind = ExactCfKind::BranchNeverLikely; return cf; } if (opcode == 0x01u && rs == 0u) { // BLTZ/BLTZAL (and likely forms) are false for zero; BGEZ forms are // true. The link-true forms are BAL/BALL aliases, so model their // target and return continuation like a call rather than a tail jump. if (rt == 0x00u || rt == 0x10u) { cf.kind = ExactCfKind::BranchNever; return cf; } if (rt == 0x02u || rt == 0x12u) { cf.kind = ExactCfKind::BranchNeverLikely; return cf; } if (rt == 0x01u || rt == 0x03u) { cf.kind = ExactCfKind::Jump; cf.target = exact_branch_target(pc, instr); return cf; } if (rt == 0x11u || rt == 0x13u) { // bal / ball aliases cf.kind = ExactCfKind::Jal; cf.target = exact_branch_target(pc, instr); return cf; } } if (opcode == 0x01u || (opcode >= 0x04u && opcode <= 0x07u) || (opcode >= 0x14u && opcode <= 0x17u)) { cf.kind = ExactCfKind::Branch; cf.target = exact_branch_target(pc, instr); return cf; } // COP1/COP2 condition branches are ordinary conditional branches. COP1 // is not present on PS1, but classifying both keeps raw control-flow and // delay-slot boundaries conservative for composite inputs. if ((opcode == 0x11u || opcode == 0x12u) && rs == 0x08u) { cf.kind = ExactCfKind::Branch; cf.target = exact_branch_target(pc, instr); return cf; } return cf; } } // namespace bool resolve_exact_bounded_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, uint32_t producer_lo, uint32_t producer_hi) { table = {}; if (entry >= hard_cap || jr_pc < entry || jr_pc >= 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 mapped = [&](uint32_t addr) { return runtime_to_image ? runtime_to_image(addr, exe) : addr; }; if (producer_lo == 0u && producer_hi == 0u) { producer_lo = exe.header.load_address; producer_hi = exe.analysis_end_address(); } if (producer_lo >= producer_hi || (producer_lo & 3u) != 0u || (producer_hi & 3u) != 0u) { return false; } auto jr_word = read(jr_pc); if (!jr_word.has_value() || *jr_word != ((jr_rs & 0x1Fu) << 21u | 0x08u)) { return false; } auto is_control = [&](uint32_t pc, uint32_t instr) { (void)pc; return exact_classify_cf(pc, instr).kind != ExactCfKind::Normal; }; auto in_delay_slot = [&](uint32_t pc) { if (pc < entry + 4u) return false; auto prev = read(pc - 4u); return prev.has_value() && is_control(pc - 4u, *prev); }; auto has_call_between = [&](uint32_t lo, uint32_t hi) { for (uint32_t pc = lo; pc < hi; pc += 4u) { auto word = read(pc); if (!word.has_value()) return true; ExactCf cf = exact_classify_cf(pc, *word); if (cf.kind == ExactCfKind::Jal || cf.kind == ExactCfKind::Jalr) { return true; } } return false; }; auto writes_between = [&](uint32_t lo, uint32_t hi, uint32_t reg) { for (uint32_t pc = lo; pc < hi; pc += 4u) { auto word = read(pc); if (!word.has_value() || exact_instruction_writes_gpr(*word, reg)) return true; } return false; }; // Reject a definition that a direct edge from before it can skip. Edges // originating after the definition are harmless: that path has already // executed the definition (Ape's self-looping out-of-range guard does this). auto inbound_skips = [&](uint32_t def_pc, uint32_t use_pc) { for (uint32_t source = entry; source < def_pc; source += 4u) { auto word = read(source); if (!word.has_value()) return true; ExactCf cf = exact_classify_cf(source, *word); bool direct = cf.kind == ExactCfKind::Branch || cf.kind == ExactCfKind::Jump || cf.kind == ExactCfKind::Jal; if (direct && cf.target > def_pc && cf.target <= use_pc) return true; } return false; }; // Keep this in parity with compile_overlays.py: the canonical scheduling // form permits zero or one NOP between LW and JR, never a raw window. uint32_t lw_pc = jr_pc - 4u, lw_base = 0; int32_t lw_offset = 0; if (lw_pc < entry) return false; auto lw_word = read(lw_pc); if (!lw_word.has_value()) return false; if (*lw_word == 0u) { if (lw_pc < entry + 4u) return false; lw_pc -= 4u; lw_word = read(lw_pc); } if (!lw_word.has_value()) return false; uint32_t lw_op = (*lw_word >> 26) & 0x3Fu; lw_base = (*lw_word >> 21) & 0x1Fu; uint32_t lw_rt = (*lw_word >> 16) & 0x1Fu; if (lw_op != 0x23u || lw_rt != jr_rs || lw_base == 0u || lw_pc < entry + 8u || in_delay_slot(lw_pc)) { return false; } lw_offset = static_cast( static_cast(*lw_word & 0xFFFFu)); // The address add and index scale must be the two instructions immediately // before the load. This deliberately excludes raw-window coincidences. uint32_t addu_pc = lw_pc - 4u; uint32_t sll_pc = addu_pc - 4u; auto addu_word = read(addu_pc); auto sll_word = read(sll_pc); if (!addu_word.has_value() || !sll_word.has_value()) return false; uint32_t addu_op = (*addu_word >> 26) & 0x3Fu; uint32_t addu_rs = (*addu_word >> 21) & 0x1Fu; uint32_t addu_rt = (*addu_word >> 16) & 0x1Fu; uint32_t addu_rd = (*addu_word >> 11) & 0x1Fu; uint32_t addu_shamt = (*addu_word >> 6) & 0x1Fu; uint32_t addu_fn = *addu_word & 0x3Fu; uint32_t sll_op = (*sll_word >> 26) & 0x3Fu; uint32_t sll_rs = (*sll_word >> 21) & 0x1Fu; uint32_t index_reg = (*sll_word >> 16) & 0x1Fu; uint32_t scaled_reg = (*sll_word >> 11) & 0x1Fu; uint32_t sll_shamt = (*sll_word >> 6) & 0x1Fu; uint32_t sll_fn = *sll_word & 0x3Fu; if (addu_op != 0u || addu_fn != 0x21u || addu_shamt != 0u || addu_rd != lw_base || sll_op != 0u || sll_fn != 0u || sll_rs != 0u || sll_shamt != 2u || index_reg == 0u || scaled_reg == 0u || (scaled_reg != addu_rs && scaled_reg != addu_rt)) { return false; } uint32_t base_reg = scaled_reg == addu_rs ? addu_rt : addu_rs; if (base_reg == 0u || base_reg == scaled_reg || in_delay_slot(addu_pc)) { return false; } // 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); if (!guard_word_opt.has_value() || !bound_word.has_value()) { return false; } uint32_t guard_word = *guard_word_opt; uint32_t guard_op = (guard_word >> 26) & 0x3Fu; uint32_t guard_rs = (guard_word >> 21) & 0x1Fu; uint32_t guard_rt = (guard_word >> 16) & 0x1Fu; uint32_t bound_reg = guard_rs == 0u ? guard_rt : guard_rs; if (guard_op != 0x04u || bound_reg == 0u || (guard_rs != 0u && guard_rt != 0u)) { return false; } uint32_t bound_op = (*bound_word >> 26) & 0x3Fu; uint32_t bound_rs = (*bound_word >> 21) & 0x1Fu; 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 || 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) || (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 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; for (uint32_t back = 1; back <= 32u; back++) { if (addu_pc < entry + back * 4u) break; uint32_t pc = addu_pc - back * 4u; auto word = read(pc); if (!word.has_value()) return false; if (!exact_instruction_writes_gpr(*word, base_reg)) continue; uint32_t op = (*word >> 26) & 0x3Fu; uint32_t rs = (*word >> 21) & 0x1Fu; uint32_t rt = (*word >> 16) & 0x1Fu; if (op == 0x09u && rt == base_reg && rs != 0u) { low_pc = pc; source_reg = rs; low = static_cast(*word & 0xFFFFu); } else if (op == 0x0Fu && rs == 0u && rt == base_reg) { lui_pc = pc; upper = (*word & 0xFFFFu) << 16; } else { return false; } break; } if (low_pc != 0u) { for (uint32_t back = 1; back <= 32u; back++) { if (low_pc < entry + back * 4u) break; uint32_t pc = low_pc - back * 4u; auto word = read(pc); if (!word.has_value()) return false; if (!exact_instruction_writes_gpr(*word, source_reg)) continue; uint32_t op = (*word >> 26) & 0x3Fu; uint32_t rs = (*word >> 21) & 0x1Fu; uint32_t rt = (*word >> 16) & 0x1Fu; if (op != 0x0Fu || rs != 0u || rt != source_reg) return false; lui_pc = pc; upper = (*word & 0xFFFFu) << 16; break; } } 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)) || has_call_between(lui_pc, addu_pc)) { return false; } // No other control transfer may intervene between the proven constant and // the canonical suffix. The bounds BEQ is the sole exception. for (uint32_t pc = lui_pc + 4u; pc < sll_pc; pc += 4u) { auto word = read(pc); if (!word.has_value() || (pc != guard_pc && is_control(pc, *word))) { return false; } } uint32_t base_def_end = low_pc != 0u ? low_pc + 4u : lui_pc + 4u; if (writes_between(base_def_end, addu_pc, base_reg) || (low_pc != 0u && writes_between(lui_pc + 4u, low_pc, source_reg))) { return false; } int64_t base64 = static_cast(upper); if (low_pc != 0u) base64 += static_cast(low); base64 += lw_offset; if (base64 < 0 || base64 > 0xFFFFFFFFll || (static_cast(base64) & 3u) != 0u) { return false; } uint32_t runtime_base = static_cast(base64); uint64_t runtime_end = static_cast(runtime_base) + static_cast(count) * 4u; if (runtime_end > 0x100000000ull) return false; ExactJumpTable resolved; resolved.table_base = runtime_base; resolved.table_count = count; resolved.targets.reserve(count); for (uint32_t i = 0; i < count; i++) { uint32_t runtime_slot = runtime_base + i * 4u; uint32_t image_slot = mapped(runtime_slot); if (image_slot < producer_lo || image_slot >= producer_hi || producer_hi - image_slot < 4u) { return false; } auto target_word = read(image_slot); if (!target_word.has_value() || (*target_word & 3u) != 0u) return false; 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; } auto target_first = read(image_target); if (!target_first.has_value() || !exact_is_valid_mips_word(*target_first)) { return false; } resolved.targets.emplace_back(runtime_target, image_target); } // Later case blocks may legitimately loop to the dispatch suffix (Ape // Escape does), but arbitrary later bytes must not create an inbound edge // that skips the reaching definitions. Prove the exemption by walking // only from the table's validated case targets. std::set case_reachable; std::queue case_work; for (const auto& target_pair : resolved.targets) case_work.push(target_pair.second); while (!case_work.empty() && case_reachable.size() < 2048u) { uint32_t pc = case_work.front(); case_work.pop(); if (case_reachable.count(pc) || pc < entry || pc >= hard_cap || pc < producer_lo || pc >= producer_hi) { continue; } auto word = read(pc); if (!word.has_value() || !exact_is_valid_mips_word(*word)) continue; case_reachable.insert(pc); ExactCf cf = exact_classify_cf(pc, *word); uint32_t delay = pc + 4u; switch (cf.kind) { case ExactCfKind::Normal: case_work.push(pc + 4u); break; case ExactCfKind::Branch: case_reachable.insert(delay); case_work.push(pc + 8u); case_work.push(cf.target); break; case ExactCfKind::BranchNever: case_reachable.insert(delay); case_work.push(pc + 8u); break; case ExactCfKind::BranchNeverLikely: case_work.push(pc + 8u); break; case ExactCfKind::Jump: case_reachable.insert(delay); case_work.push(cf.target); break; case ExactCfKind::Jal: case ExactCfKind::Jalr: case_reachable.insert(delay); case_work.push(pc + 8u); break; case ExactCfKind::JrRa: case ExactCfKind::JrOther: case_reachable.insert(delay); 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; ExactCf cf = exact_classify_cf(source, *word); bool direct = cf.kind == ExactCfKind::Branch || cf.kind == ExactCfKind::Jump || cf.kind == ExactCfKind::Jal; if (direct && cf.target > protected_pc && cf.target <= jr_pc && (source < protected_pc || source > jr_pc) && (scheduled_base || !case_reachable.count(source))) { return false; } } table = std::move(resolved); return true; } bool resolve_computed_stride_jump( 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; } // Nearest definition of `reg` strictly before `use_pc`, walking back at // most `window` instructions through straight-line code only. A control // transfer (or its delay slot) in the way ends the search: the chain // must be one basic block so every path to the jr computes the same // shape. 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; }; auto fields = [&](uint32_t w, uint32_t& op, uint32_t& rs, uint32_t& rt, uint32_t& rd, uint32_t& sa, uint32_t& fn) { op = (w >> 26) & 0x3Fu; rs = (w >> 21) & 0x1Fu; rt = (w >> 16) & 0x1Fu; rd = (w >> 11) & 0x1Fu; sa = (w >> 6) & 0x1Fu; fn = w & 0x3Fu; }; auto is_addu = [&](uint32_t w, uint32_t& rd, uint32_t& rs, uint32_t& rt) { uint32_t op, sa, fn; fields(w, op, rs, rt, rd, sa, fn); return op == 0u && fn == 0x21u && sa == 0u && rd != 0u; }; // sll rd, rt, sa -> (rd, index rt, shift) auto is_sll = [&](uint32_t w, uint32_t& rd, uint32_t& idx, uint32_t& sh) { uint32_t op, rs, fn; fields(w, op, rs, idx, rd, sh, fn); return op == 0u && fn == 0u && rs == 0u && rd != 0u && idx != 0u && w != 0u; }; // 1. jr_rs = addu B, S (either order), nearest definition before the jr. auto addu_pc = nearest_def(jr_rs, jr_pc, 8u); if (!addu_pc.has_value()) return false; uint32_t a_rd, a_rs, a_rt; if (!is_addu(*read(*addu_pc), a_rd, a_rs, a_rt) || a_rs == a_rt || a_rs == 0u || a_rt == 0u) { return false; } // 2. One operand is the scaled index: sll S,I,k or the two-sll sum. // Try each operand as the scaled register; the other is the base. auto scaled_stride = [&](uint32_t s_reg, uint32_t use_pc) -> uint32_t { auto def = nearest_def(s_reg, use_pc, 8u); if (!def.has_value()) return 0u; uint32_t w = *read(*def); uint32_t rd, idx, sh; if (is_sll(w, rd, idx, sh)) { return (sh >= 2u && sh <= 8u) ? (1u << sh) : 0u; } uint32_t s_rd, p, q; if (!is_addu(w, s_rd, p, q) || p == q || p == 0u || q == 0u) return 0u; auto p_def = nearest_def(p, *def, 8u); auto q_def = nearest_def(q, *def, 8u); if (!p_def.has_value() || !q_def.has_value()) return 0u; uint32_t p_rd, p_idx, p_sh, q_rd, q_idx, q_sh; if (!is_sll(*read(*p_def), p_rd, p_idx, p_sh) || !is_sll(*read(*q_def), q_rd, q_idx, q_sh) || p_idx != q_idx || p_sh == q_sh || p_sh > 8u || q_sh > 8u) { return 0u; } // The index must reach both shifts unchanged: no write to it between // the earlier sll and the later one. uint32_t lo = std::min(*p_def, *q_def), hi = std::max(*p_def, *q_def); for (uint32_t pc = lo + 4u; pc < hi; pc += 4u) { if (exact_instruction_writes_gpr(*read(pc), p_idx)) return 0u; } return (1u << p_sh) + (1u << q_sh); }; uint32_t stride = scaled_stride(a_rt, *addu_pc); if (stride == 0u) stride = scaled_stride(a_rs, *addu_pc); if (stride == 0u || (stride & 3u) != 0u || stride > 256u) return false; // 3. The run: N >= 2 shape-identical groups right after the delay slot. // Same opcode/register fields per position; immediates free; no // control flow inside group 0 (so every group is straight-line). const uint32_t run_start = jr_pc + 8u; const uint32_t words_per_group = stride / 4u; if (run_start + stride > hard_cap) return false; auto shape = [](uint32_t w) -> uint32_t { uint32_t op = (w >> 26) & 0x3Fu; // I-type: keep opcode + rs + rt, drop the immediate. J-type and // SPECIAL/REGIMM/COP forms compare whole. if (op == 0u || op == 1u || op == 2u || op == 3u || op == 0x10u || op == 0x12u) { return w; } return w & 0xFFFF0000u; }; std::vector group0; bool all_nop = true; for (uint32_t i = 0; i < words_per_group; ++i) { auto w = read(run_start + i * 4u); if (!w.has_value()) return false; if (exact_classify_cf(run_start + i * 4u, *w).kind != ExactCfKind::Normal) return false; if (*w != 0u) all_nop = false; group0.push_back(*w); } if (all_nop) return false; uint32_t groups = 1; for (;;) { uint32_t g = run_start + groups * stride; if (g + stride > hard_cap) break; bool same = true; for (uint32_t i = 0; i < words_per_group && same; ++i) { auto w = read(g + i * 4u); same = w.has_value() && shape(*w) == shape(group0[i]); } if (!same) break; ++groups; } if (groups < 2u) return false; auto mapped = [&](uint32_t addr) { return runtime_to_image ? runtime_to_image(addr, exe) : addr; }; ExactJumpTable resolved; resolved.table_base = run_start; resolved.stride = stride; for (uint32_t k = 0; k <= groups; ++k) { uint32_t t = run_start + k * stride; if (t >= hard_cap) break; resolved.targets.emplace_back(t, mapped(t)); } resolved.table_count = static_cast(resolved.targets.size()); table = std::move(resolved); 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, const std::set& cross_call_allow, bool no_split_guard, const std::set& trusted_entries) { FunctionAnalysisResult result; result.total_instructions = 0; result.jr_ra_count = 0; result.prologue_count = 0; result.call_discovered_count = 0; result.state_continuation_count = 0; fmt::print("\n=== Exact-Entry Function Analysis ===\n\n"); // Discovery bound, NOT the read bound. An overlay capture deliberately // carries trailing delay-slot guard words (ps1_exe_parser.h exe_tag): // readable so a branch at the last analysable word can emit its slot, but // never admissible as code themselves, because the word after a guard // word does not exist in the image. auto in_exe = [&](uint32_t addr) { return addr >= exe_.header.load_address && addr < exe_.analysis_end_address() && (addr & 3u) == 0; }; auto producer_for = [&](uint32_t addr) -> int { for (size_t i = 0; i < producer_ranges.size(); i++) { if (addr >= producer_ranges[i].first && addr < producer_ranges[i].second) return static_cast(i); } return -1; }; auto callable_direct_jal_target = [&](uint32_t source, uint32_t addr) { auto word_opt = exe_.read_word(addr); // A JAL encountered by the reachable CFG walk is direct call evidence. // Requiring a stack prologue here discarded legitimate frameless leaf // functions and tiny library wrappers. The target still has to decode // as a valid PS1 instruction and lie inside the verified image bound. // // Three adjacent aligned words that all point back into this image are // stronger evidence for a position-fixed pointer table than the first // word's accidental MIPS decode. Do not follow a JAL-shaped word into // that data. The caller remains valid and dispatches the unresolved // target at runtime, where the ordinary live-byte guard can select a // separately proven implementation or fall back to the interpreter. if (!word_opt.has_value() || !exact_is_valid_mips_word(*word_opt)) return false; bool dense_local_pointer_table = true; for (uint32_t i = 0; i < 3u; i++) { auto value = exe_.read_word(addr + i * 4u); if (!value.has_value() || (*value & 3u) != 0u || !in_exe(*value)) { dense_local_pointer_table = false; break; } } if (dense_local_pointer_table) return false; if (producer_ranges.empty()) return true; int source_producer = producer_for(source); int target_producer = producer_for(addr); // A composite's uncovered padding is not a producer. In particular, // two gap addresses both map to -1 and must not be treated as sharing // ownership, nor may an allow-list bless a target with no producer. if (source_producer < 0 || target_producer < 0) return false; if (source_producer == target_producer) return true; return cross_call_allow.count(addr) != 0; }; auto walk = [&](uint32_t entry, uint32_t hard_cap) { ExactWalkResult wr; std::queue work; work.push(entry); const int entry_producer = producer_for(entry); auto in_function = [&](uint32_t addr) { return in_exe(addr) && addr >= entry && addr < hard_cap && (producer_ranges.empty() || (entry_producer >= 0 && producer_for(addr) == entry_producer)); }; while (!work.empty()) { uint32_t pc = work.front(); work.pop(); if (wr.visited.count(pc) || !in_function(pc)) continue; auto word_opt = exe_.read_word(pc); if (!word_opt.has_value()) continue; uint32_t instr = *word_opt; wr.visited.insert(pc); ExactCf cf = exact_classify_cf(pc, instr); uint32_t delay = pc + 4u; switch (cf.kind) { case ExactCfKind::Normal: if (in_function(pc + 4u)) work.push(pc + 4u); break; case ExactCfKind::Branch: if (in_function(delay)) wr.visited.insert(delay); if (in_function(pc + 8u)) work.push(pc + 8u); if (in_function(cf.target)) work.push(cf.target); break; case ExactCfKind::BranchNever: if (in_function(delay)) wr.visited.insert(delay); if (in_function(pc + 8u)) work.push(pc + 8u); break; case ExactCfKind::BranchNeverLikely: if (in_function(pc + 8u)) work.push(pc + 8u); break; case ExactCfKind::Jump: if (in_function(delay)) wr.visited.insert(delay); if (in_function(cf.target)) work.push(cf.target); break; case ExactCfKind::Jal: if (in_function(delay)) wr.visited.insert(delay); if (in_function(pc + 8u)) work.push(pc + 8u); if (in_exe(cf.target) && callable_direct_jal_target(pc, cf.target)) { wr.direct_jal_targets.insert(cf.target); wr.transfer_sources[cf.target].insert( std::make_pair(pc, false)); } break; case ExactCfKind::Jalr: if (in_function(delay)) wr.visited.insert(delay); if (in_function(pc + 8u)) work.push(pc + 8u); { uint32_t target = 0; uint32_t target_reg = (instr >> 21) & 0x1Fu; if (exact_resolve_constant_transfer(exe_, entry, pc, target_reg, target) && in_exe(target) && callable_direct_jal_target(pc, target)) { wr.direct_jal_targets.insert(target); wr.transfer_sources[target].insert( std::make_pair(pc, true)); } } break; case ExactCfKind::JrRa: if (in_function(delay)) wr.visited.insert(delay); wr.jr_ra_count++; break; case ExactCfKind::JrOther: if (in_function(delay)) wr.visited.insert(delay); { uint32_t jr_rs = (instr >> 21) & 0x1Fu; uint32_t target = 0; if (exact_resolve_constant_transfer(exe_, entry, pc, jr_rs, target)) { if (in_function(target)) { wr.jump_table_targets.insert(target); work.push(target); } else if (in_exe(target) && callable_direct_jal_target(pc, target)) { // An absolute JR out of the current function is a // statically proven tail-call entry. wr.direct_jal_targets.insert(target); wr.transfer_sources[target].insert( std::make_pair(pc, true)); } } else { ExactJumpTable table; uint32_t producer_lo = exe_.header.load_address; uint32_t producer_hi = exe_.analysis_end_address(); if (entry_producer >= 0) { producer_lo = producer_ranges[entry_producer].first; producer_hi = producer_ranges[entry_producer].second; } if (resolve_exact_bounded_jump_table( 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(), [&](const auto& target_pair) { return in_function(target_pair.second); }); if (all_owned) { for (const auto& target_pair : table.targets) { uint32_t target_pc = target_pair.second; wr.jump_table_targets.insert(target_pc); work.push(target_pc); } } } // Otherwise fail closed. Unresolved JR cases remain // interpreted until the complete dependency chain and // every table entry are proven. } } break; } } return wr; }; std::set known_entries; for (uint32_t entry : entries) { if (!in_exe(entry)) continue; if (!producer_ranges.empty() && producer_for(entry) < 0) continue; known_entries.insert(entry); } size_t explicit_count = known_entries.size(); const std::set explicit_entries = known_entries; std::set derived_entries; std::map>> derived_evidence; fmt::print("Explicit entries: {}\n", explicit_count); // No-split guard: fixed point over EVERY non-trusted entry (explicit and // derived); same rule as compile_overlays.no_split_partition. For each // host, the run of following entries its UNCAPPED walk reaches (stopping // at the first it does not reach, or a trusted entry) may be inside it; // walk the host capped at the end of the run, shrink the run to the first // entry that walk misses, and absorb the rest together. Absorbing the run // at once lets a switch whose table targets several entries resolve // (every target must lie inside the walk); stopping at the first // unreached entry keeps a forward tail call over an unrelated function // from orphaning its target. Absorbing only removes caps, so repeat. auto absorb_split_entries = [&](std::set& entries) { const uint32_t analysis_end = exe_.analysis_end_address(); bool changed = true; while (changed) { changed = false; std::vector roots(entries.begin(), entries.end()); std::vector kept; size_t index = 0; while (index < roots.size()) { const uint32_t host = roots[index]; kept.push_back(host); const ExactWalkResult reach = walk(host, analysis_end); // A wider range can reject a jump table, so the walk capped // at the successor is not always a subset: consult both. const ExactWalkResult succ = index + 1 < roots.size() ? walk(host, roots[index + 1]) : ExactWalkResult{}; auto reached = [&](uint32_t a) { return reach.visited.count(a) || succ.visited.count(a); }; size_t end = index + 1; while (end < roots.size() && reached(roots[end]) && !trusted_entries.count(roots[end])) end++; while (end > index + 1) { const uint32_t cap = end < roots.size() ? roots[end] : analysis_end; const ExactWalkResult inner = walk(host, cap); size_t blocked = end; for (size_t k = index + 1; k < end; k++) { if (!inner.visited.count(roots[k])) { blocked = k; break; } } if (blocked == end) break; end = blocked; } if (end > index + 1) changed = true; index = end; } entries = std::set(kept.begin(), kept.end()); } }; if (no_split_guard) absorb_split_entries(known_entries); // Discover callees in rounds against one stable entry partition. Processing // roots sequentially let an early caller insert a JAL target that actually // lay inside a later explicit root. That new entry hard-capped the later // root before it was walked (Tomba X01's jump-table host), splitting one // function and losing every case after the accidental cap. If a candidate // is already owned by any walk in the final partition, leave it absorbed; // the analyzer exports that proven host mapping for main_psx to materialize. uint32_t discovery_round = 0; std::set, std::set>> seen_states; while (true) { auto state = std::make_pair(known_entries, derived_entries); if (++discovery_round > 64u || !seen_states.insert(state).second) { fmt::print("WARNING: exact-entry ownership did not converge; " "falling back to explicit roots only\n"); known_entries = explicit_entries; if (no_split_guard) absorb_split_entries(known_entries); derived_entries.clear(); derived_evidence.clear(); break; } const std::set previous_entries = known_entries; const std::set previous_derived = derived_entries; std::vector round_entries( known_entries.begin(), known_entries.end()); std::set candidate_targets; std::map>> candidate_evidence; for (size_t i = 0; i < round_entries.size(); i++) { uint32_t hard_cap = (i + 1 < round_entries.size()) ? round_entries[i + 1] : exe_.analysis_end_address(); ExactWalkResult wr = walk(round_entries[i], hard_cap); candidate_targets.insert(wr.direct_jal_targets.begin(), wr.direct_jal_targets.end()); for (const auto& [target, evidence] : wr.transfer_sources) candidate_evidence[target].insert( evidence.begin(), evidence.end()); } derived_entries.clear(); derived_evidence.clear(); for (uint32_t target : candidate_targets) if (!explicit_entries.count(target)) { derived_entries.insert(target); auto evidence = candidate_evidence.find(target); if (evidence != candidate_evidence.end()) derived_evidence.emplace(target, evidence->second); } // Rebuild ownership FROM SCRATCH whenever the candidate universe grows. // A target B absorbed by A in one round may need to become a root after // a later target X lands between A and B and caps A. A monotonic // "absorbed forever" set therefore loses reachable code. known_entries = explicit_entries; known_entries.insert(derived_entries.begin(), derived_entries.end()); if (no_split_guard) { absorb_split_entries(known_entries); } else for (uint32_t target : derived_entries) { auto target_it = known_entries.find(target); if (target_it == known_entries.end() || explicit_entries.count(target) || target_it == known_entries.begin()) { continue; } auto owner_it = std::prev(target_it); auto next_it = std::next(target_it); uint32_t hard_cap = next_it != known_entries.end() ? *next_it : exe_.analysis_end_address(); ExactWalkResult owner_walk = walk(*owner_it, hard_cap); if (owner_walk.visited.count(target)) { known_entries.erase(target_it); } } if (known_entries == previous_entries && derived_entries == previous_derived) break; } size_t explicit_kept = 0; for (uint32_t entry : known_entries) explicit_kept += explicit_entries.count(entry); result.call_discovered_count = static_cast(known_entries.size() - explicit_kept); fmt::print("Direct-JAL entries: {}\n", result.call_discovered_count); if (no_split_guard) fmt::print("No-split guard absorbed explicit entries: {}\n", explicit_count - explicit_kept); fmt::print("Total exact entries: {}\n\n", known_entries.size()); std::vector starts_vec(known_entries.begin(), known_entries.end()); std::sort(starts_vec.begin(), starts_vec.end()); for (size_t i = 0; i < starts_vec.size(); i++) { uint32_t entry = starts_vec[i]; // The final entry's cap is the ANALYSIS end, not the image end, so a // walk that falls through the last real word of an overlay capture // stops there instead of absorbing the trailing guard word into // func.end_addr (which made the guard word the last block's leader). uint32_t hard_cap = (i + 1 < starts_vec.size()) ? starts_vec[i + 1] : exe_.analysis_end_address(); ExactWalkResult wr = walk(entry, hard_cap); if (wr.visited.empty()) continue; Function func; func.start_addr = entry; func.end_addr = *wr.visited.rbegin() + 4u; if (func.end_addr > hard_cap) func.end_addr = hard_cap; if (func.end_addr <= func.start_addr) func.end_addr = func.start_addr + 4u; func.size = func.end_addr - func.start_addr; func.name = fmt::format("func_{:08X}", func.start_addr); if (producer_for(entry) >= 0) { const auto& producer = producer_ranges[producer_for(entry)]; func.producer_lo = producer.first; func.producer_hi = producer.second; } auto first_instr = exe_.read_word(func.start_addr); if (first_instr.has_value()) { int32_t stack_size; func.has_prologue = is_prologue(*first_instr, stack_size); func.stack_frame_size = func.has_prologue ? stack_size : 0; if (func.has_prologue) result.prologue_count++; } else { func.has_prologue = false; func.stack_frame_size = 0; } func.has_epilogue = wr.jr_ra_count > 0; func.is_data_section = false; result.jr_ra_count += static_cast(wr.jr_ra_count); result.total_instructions += static_cast(wr.visited.size()); result.exact_reachable_pcs.insert(wr.visited.begin(), wr.visited.end()); result.functions.push_back(func); } // Export only analyzer-proven aliases with a host in the FINAL partition. // Re-walking final roots makes stale ownership impossible: if a later root // now caps the old host before B, B is either a real root or absent here. std::set final_starts(known_entries.begin(), known_entries.end()); std::vector> final_walks; for (size_t i = 0; i < starts_vec.size(); i++) { uint32_t host_start = starts_vec[i]; uint32_t hard_cap = (i + 1 < starts_vec.size()) ? starts_vec[i + 1] : exe_.analysis_end_address(); ExactWalkResult wr = walk(host_start, hard_cap); if (wr.visited.empty()) continue; uint32_t host_end = *wr.visited.rbegin() + 4u; Function host{}; host.start_addr = host_start; host.end_addr = host_end; final_walks.push_back({host, std::move(wr)}); } for (uint32_t target : derived_entries) { if (final_starts.count(target)) continue; for (const auto& [host, wr] : final_walks) { if (!wr.visited.count(target)) continue; auto evidence = derived_evidence.find(target); if (evidence == derived_evidence.end()) break; auto source = std::find_if( evidence->second.begin(), evidence->second.end(), [&](const auto& item) { return wr.visited.count(item.first); }); if (source == evidence->second.end()) break; result.absorbed_entries.push_back( {target, host.start_addr, host.end_addr, source->first, source->second}); break; } } // Explicit entries the no-split guard absorbed: the proof is the host's // own walk (fallthrough/branch), so the host start is the source. A // delay-slot target is not exported: an alias there would make the slot // a block leader. It stays with the interpreter. if (no_split_guard) { for (uint32_t target : explicit_entries) { if (final_starts.count(target)) continue; if (target >= exe_.header.load_address + 4u) { auto prev = exe_.read_word(target - 4u); if (prev.has_value() && is_branch_or_jump(*prev)) continue; } for (const auto& [host, wr] : final_walks) { if (!wr.visited.count(target)) continue; result.absorbed_entries.push_back( {target, host.start_addr, host.end_addr, host.start_addr, false}); break; } } } return result; } FunctionAnalysisResult FunctionAnalyzer::analyze() { FunctionAnalysisResult result; result.total_instructions = 0; result.jr_ra_count = 0; result.prologue_count = 0; result.call_discovered_count = 0; result.strong_prologue_count = 0; result.bios_thunk_count = 0; result.state_continuation_count = 0; fmt::print("\n=== Function Boundary Detection ===\n\n"); // Pass 1: Find all jr $ra instructions std::vector return_addresses; uint32_t current_addr = exe_.header.load_address; uint32_t end_addr = exe_.analysis_end_address(); fmt::print("Scanning {} KB of code for function returns...\n", (end_addr - current_addr) / 1024); while (current_addr < end_addr) { auto word_opt = exe_.read_word(current_addr); if (!word_opt.has_value()) { break; } uint32_t instr = *word_opt; result.total_instructions++; if (is_jr_ra(instr)) { return_addresses.push_back(current_addr); result.jr_ra_count++; } int32_t stack_size; if (is_prologue(instr, stack_size)) { result.prologue_count++; } current_addr += 4; } fmt::print("✓ Found {} jr $ra instructions\n", result.jr_ra_count); fmt::print("✓ Found {} function prologues\n", result.prologue_count); fmt::print("✓ Scanned {} total instructions\n\n", result.total_instructions); // Pass 2: For each jr $ra, find function boundaries fmt::print("Analyzing function boundaries...\n"); std::set function_starts; // Use set to avoid duplicates std::map function_last_return; for (uint32_t return_addr : return_addresses) { uint32_t func_start = find_function_start(return_addr); function_starts.insert(func_start); auto it = function_last_return.find(func_start); if (it == function_last_return.end() || return_addr > it->second) { function_last_return[func_start] = return_addr; } } size_t jr_ra_discovered = function_starts.size(); fmt::print("✓ Identified {} unique functions from jr $ra scan\n", jr_ra_discovered); // Pass 2.5: Follow JAL call targets to discover additional functions // This finds functions that don't have standard jr $ra prologues fmt::print("Following JAL call targets to discover additional functions...\n"); uint32_t exe_start = exe_.header.load_address; uint32_t exe_end = exe_.analysis_end_address(); for (uint32_t addr = exe_start; addr < exe_end; addr += 4) { auto word_opt = exe_.read_word(addr); if (!word_opt.has_value()) break; uint32_t instr = *word_opt; uint32_t opcode = (instr >> 26) & 0x3F; if (opcode == 3) { // JAL // PS1 JAL target: upper 4 bits from the delay slot's PC, so a // KUSEG-linked image calls KUSEG targets (§5.3). uint32_t target = ((addr + 4u) & 0xF0000000u) | ((instr & 0x03FFFFFFu) << 2); // Only add if target is within EXE range and word-aligned if (target >= exe_start && target < exe_end && (target & 3) == 0) { auto target_word = exe_.read_word(target); int32_t target_stack_size = 0; if (target_word.has_value() && is_prologue(*target_word, target_stack_size)) { uint32_t setup_start = 0; if (find_preprologue_setup_start(exe_, target, exe_start, setup_start)) { target = setup_start; } } function_starts.insert(target); } } } result.call_discovered_count = static_cast(function_starts.size() - jr_ra_discovered); fmt::print("✓ Identified {} unique functions ({} call-discovered)\n\n", function_starts.size(), result.call_discovered_count); // Pass 2.55: promote strong standalone prologues. // // Return-scanning alone misses scheduler/task entry points that never // return normally, and direct JAL following misses callback entries whose // addresses are derived at runtime. A standard stack-frame prologue that // quickly saves $ra is a strong function-entry signal, provided the ADDIU // is not itself a branch delay slot. fmt::print("Finding strong standalone prologue entries...\n"); int strong_prologues = 0; for (uint32_t addr = exe_start; addr < exe_end; addr += 4) { auto word_opt = exe_.read_word(addr); if (!word_opt.has_value()) break; int32_t stack_size = 0; if (!is_prologue(*word_opt, stack_size)) continue; if (addr >= exe_start + 4) { auto prev_opt = exe_.read_word(addr - 4); if (prev_opt.has_value() && is_branch_or_jump(*prev_opt)) { continue; } } uint32_t entry_addr = addr; uint32_t setup_start = 0; if (find_preprologue_setup_start(exe_, addr, exe_start, setup_start)) { entry_addr = setup_start; } auto existing_it = function_starts.upper_bound(entry_addr); if (existing_it != function_starts.begin()) { --existing_it; auto return_it = function_last_return.find(*existing_it); if (return_it != function_last_return.end() && return_it->second + 8u > entry_addr) { continue; } } bool saves_ra = false; for (uint32_t look = addr + 4; look < addr + 64 && look < exe_end; look += 4) { auto next_opt = exe_.read_word(look); if (!next_opt.has_value()) break; if (is_sw_reg_sp(*next_opt, 31)) { saves_ra = true; break; } if (is_branch_or_jump(*next_opt)) { break; } } if (saves_ra && function_starts.insert(entry_addr).second) { if (entry_addr != addr) { function_starts.erase(addr); } strong_prologues++; } } result.strong_prologue_count = strong_prologues; fmt::print("Identified {} strong prologue entries\n\n", result.strong_prologue_count); // Pass 2.56: promote packed PSY-Q BIOS dispatch thunks. // // PSY-Q libraries often pack tiny A0/B0/C0 syscall thunks back-to-back: // addiu rN, $zero, 0xA0/0xB0/0xC0 // jr rN // addiu $t1, $zero, index // // They do not contain jr $ra and may sit directly before data tables, so // return/prologue scanning either misses them or lets the following data // make the region look like a data section. Treat each thunk as a real // function and cap it after the JR delay slot. fmt::print("Finding packed BIOS dispatch thunk entries...\n"); int bios_thunks = 0; int bios_thunks_skipped = 0; for (uint32_t addr = exe_start; addr + 12u <= exe_end; addr += 4) { uint32_t jr_addr = 0; if (!is_bios_dispatch_thunk(addr, jr_addr)) continue; // A li/jr BIOS-dispatch sequence is only a STANDALONE thunk when it // sits in a gap that earlier discovery (prologue scan, jr-$ra scan, // call-target following) missed. A real function may legitimately // *begin* with this sequence (a tail call into the BIOS) yet continue // with reachable code — e.g. a libcard routine that calls the BIOS and // then polls card status. Treating such a function as a thunk and // capping function_last_return at its first JR silently truncates the // rest of its body. That is exactly what gutted Tomba's card-poll // routine func_8005CDB8 (104B -> 12B) and stalled LOAD GAME. // // Mirror segagenesisrecomp's boundary-split discipline: a pattern // heuristic must never shrink or split an already-discovered function. // Only promote+cap thunks that are NOT already a known entry and do // NOT fall inside an existing function's discovered extent. if (function_starts.count(addr)) { // Already discovered as a real function start — leave its extent // alone; do not re-cap it to the first JR. bios_thunks_skipped++; continue; } auto owner_it = function_starts.upper_bound(addr); // first start > addr if (owner_it != function_starts.begin()) { --owner_it; // greatest start <= addr uint32_t owner = *owner_it; auto ret_it = function_last_return.find(owner); if (owner < addr && ret_it != function_last_return.end() && addr <= ret_it->second) { // Inside an existing function's body — promoting here would // split that function. Leave it whole. bios_thunks_skipped++; continue; } } if (function_starts.insert(addr).second) { bios_thunks++; } auto it = function_last_return.find(addr); if (it == function_last_return.end() || jr_addr > it->second) { function_last_return[addr] = jr_addr; } } result.bios_thunk_count = bios_thunks; fmt::print("Identified {} packed BIOS dispatch thunk entries " "({} candidates skipped: already discovered / inside a function)\n\n", result.bios_thunk_count, bios_thunks_skipped); // Pass 2.6: discover continuations saved by setjmp/SaveState-style helpers. // // Some games save the caller's $ra into an application context object and // later restore it from an exception/VSync callback. The restored PC is the // instruction after the original JAL delay slot, not a normal prologue, so // it must be available as a split dispatch entry. fmt::print("Finding SaveState-style continuation entries...\n"); int state_continuations = 0; for (uint32_t addr = exe_start; addr < exe_end; addr += 4) { auto word_opt = exe_.read_word(addr); if (!word_opt.has_value()) break; uint32_t instr = *word_opt; uint32_t opcode = (instr >> 26) & 0x3F; if (opcode != 3) continue; // JAL uint32_t target = ((addr + 4u) & 0xF0000000u) | ((instr & 0x03FFFFFFu) << 2); uint32_t cont = addr + 8; if (target < exe_start || target >= exe_end || (target & 3) != 0) continue; if (cont < exe_start || cont >= exe_end || (cont & 3) != 0) continue; auto target_first = exe_.read_word(target); if (!target_first.has_value()) continue; // sw $ra, imm($a0) at callee entry is the narrow signature for these // context-save helpers. It avoids splitting ordinary calls. if (is_sw_reg_base(*target_first, 4, 31)) { bool inserted = function_starts.insert(cont).second; if (inserted) state_continuations++; } } result.state_continuation_count = state_continuations; fmt::print("Identified {} SaveState continuation entries\n\n", result.state_continuation_count); // Pass 2.65: promote executable pointer-table entries. // // Many PS1 libraries store callback vectors as raw function addresses in // the loaded EXE image. Some callback entries perform a few global loads // before allocating their stack frame, so prologue-only discovery emits // the later ADDIU $sp address while the game calls the earlier table // address. Treat aligned image words that point at a near-prologue entry // as first-class function starts, and collapse the later prologue-only // start when it belongs to that prelude. fmt::print("Finding executable pointer-table function entries...\n"); std::map pointer_refs; for (uint32_t addr = exe_start; addr < exe_end; addr += 4) { auto word_opt = exe_.read_word(addr); if (!word_opt.has_value()) break; uint32_t target = *word_opt; if ((target & 3u) != 0) continue; if (target < exe_start || target >= exe_end) continue; uint32_t prologue_addr = 0; if (!pointer_target_has_near_prologue(exe_, target, exe_start, exe_end, prologue_addr)) { continue; } pointer_refs[target]++; if (prologue_addr != target) { auto prologue_it = function_starts.find(prologue_addr); if (prologue_it != function_starts.end()) { auto ret_it = function_last_return.find(prologue_addr); if (ret_it != function_last_return.end()) { auto dst_it = function_last_return.find(target); if (dst_it == function_last_return.end() || ret_it->second > dst_it->second) { function_last_return[target] = ret_it->second; } function_last_return.erase(ret_it); } function_starts.erase(prologue_it); } } } int pointer_entries = 0; for (const auto& [target, refs] : pointer_refs) { (void)refs; if (function_starts.insert(target).second) { pointer_entries++; } } result.pointer_table_entry_count = pointer_entries; fmt::print("Identified {} pointer-table function entries\n\n", result.pointer_table_entry_count); // Pass 2.7: Add forced entry points // These are function starts that do not have a standard prologue (e.g. the // PS1 EXE entry point, which is launched directly by the BIOS without a // JAL and starts with a BSS-zeroing loop rather than ADDIU $sp, $sp, -N). if (!forced_entry_points_.empty()) { fmt::print("Adding {} forced entry point(s)...\n", forced_entry_points_.size()); for (uint32_t addr : forced_entry_points_) { bool inserted = function_starts.insert(addr).second; if (inserted) { fmt::print(" Forced entry 0x{:08X} added\n", addr); } else { fmt::print(" Forced entry 0x{:08X} already present (skipped)\n", addr); } } fmt::print("\n"); } // Pass 3: Build function table with details std::vector starts_vec(function_starts.begin(), function_starts.end()); std::sort(starts_vec.begin(), starts_vec.end()); // ── Control-flow-aware function extent + code/data classification ── // // The legacy estimate set end_addr to the NEXT discovered function start // and then ran is_likely_data_section over that whole span. In Psy-Q / // Capcom titles (e.g. Mega Man X4) a real function is immediately followed // by an INLINE data / jump table (build-signature strings, dispatch // tables). That trailing data inflated the function's measured extent, and // is_likely_data_section's undefined-opcode density then misclassified the // real function as a data section — emitting a psx_unknown_dispatch stub // for real, directly-called code (the entire 0x800EE3A4.. text tail, // ~261 KB / 7,373 entries, became stubs, and the boot jal into it hit the // stub → fail-fast). // // Fix (mirrors the proven analyze_exact_entries overlay walk): walk each // function's control flow from its entry to its real terminus (the jr $ra / // tail-jump reachable from the entry). If the reachable body is provably // clean code — reaches a return/tail exit, decodes with zero invalid // primary opcodes, and every indirect jump was resolved to in-function // targets — bound end_addr to that terminus (so trailing inline data is // excluded and never linearly disassembled as code) and mark it code. Only // functions that are NOT provably clean code fall back to the legacy // boundary + density heuristic, which still correctly rejects genuine data // tables minted as function starts (those never walk as clean code: // ~44% of random data words carry an invalid primary opcode). auto in_exe_p3 = [&](uint32_t a) { return a >= exe_.header.load_address && a < exe_.analysis_end_address() && (a & 3u) == 0; }; // Same primary-opcode validity table as is_likely_data_section so the two // agree on what "clean code" means (COP2/GTE, LWC2/SWC2 are valid PS1 ops). auto valid_primary_p3 = [](uint32_t instr) -> bool { static const bool valid[64] = { true, true, true, true, true, true, true, true, // 0x00-0x07 true, true, true, true, true, true, true, true, // 0x08-0x0F true, false, true, false, false, false, false, false, // 0x10-0x17 false, false, false, false, false, false, false, false, // 0x18-0x1F true, true, true, true, true, true, true, false, // 0x20-0x27 true, true, true, true, false, false, true, false, // 0x28-0x2F true, false, true, false, false, false, false, false, // 0x30-0x37 true, false, true, false, false, false, false, false, // 0x38-0x3F }; return valid[(instr >> 26) & 0x3Fu]; }; // Mirror exact-entry discovery's strict bounded-switch recognizer so the // final extent cannot disagree about which JR targets are code. auto resolve_jt_p3 = [&](uint32_t entry, uint32_t hard_cap, uint32_t jr_pc, uint32_t jr_rs, std::vector& targets) -> bool { ExactJumpTable table; if (!resolve_exact_bounded_jump_table( exe_, entry, hard_cap, jr_pc, jr_rs, table)) { return false; } for (const auto& target_pair : table.targets) targets.push_back(target_pair.second); return true; }; struct ExtentP3 { uint32_t terminus; bool clean_code; bool hit_invalid; bool resolved_jt; bool unresolved_indirect; }; auto compute_extent_p3 = [&](uint32_t entry, uint32_t hard_cap) -> ExtentP3 { std::set visited; std::queue work; work.push(entry); bool hit_invalid = false, reached_exit = false, unresolved_indirect = false; bool resolved_jt = false; auto in_fn = [&](uint32_t a) { return in_exe_p3(a) && a >= entry && a < hard_cap; }; while (!work.empty()) { uint32_t pc = work.front(); work.pop(); if (visited.count(pc) || !in_fn(pc)) continue; auto word_opt = exe_.read_word(pc); if (!word_opt.has_value()) continue; uint32_t instr = *word_opt; visited.insert(pc); if (!valid_primary_p3(instr)) { hit_invalid = true; continue; } ExactCf cf = exact_classify_cf(pc, instr); uint32_t delay = pc + 4u; switch (cf.kind) { case ExactCfKind::Normal: if (in_fn(pc + 4u)) work.push(pc + 4u); break; case ExactCfKind::Branch: if (in_fn(delay)) visited.insert(delay); if (in_fn(pc + 8u)) work.push(pc + 8u); if (in_fn(cf.target)) work.push(cf.target); break; case ExactCfKind::BranchNever: if (in_fn(delay)) visited.insert(delay); if (in_fn(pc + 8u)) work.push(pc + 8u); break; case ExactCfKind::BranchNeverLikely: if (in_fn(pc + 8u)) work.push(pc + 8u); break; case ExactCfKind::Jump: if (in_fn(delay)) visited.insert(delay); if (in_fn(cf.target)) work.push(cf.target); else reached_exit = true; // tail call / jump out of function break; case ExactCfKind::Jal: case ExactCfKind::Jalr: if (in_fn(delay)) visited.insert(delay); if (in_fn(pc + 8u)) work.push(pc + 8u); break; case ExactCfKind::JrRa: if (in_fn(delay)) visited.insert(delay); reached_exit = true; break; case ExactCfKind::JrOther: { if (in_fn(delay)) visited.insert(delay); uint32_t jr_rs = (instr >> 21) & 0x1Fu; uint32_t constant_target = 0; if (exact_resolve_constant_transfer(exe_, entry, pc, jr_rs, constant_target)) { resolved_jt = true; if (in_fn(constant_target)) work.push(constant_target); else reached_exit = true; } else { std::vector jt; if (resolve_jt_p3(entry, hard_cap, pc, jr_rs, jt)) { resolved_jt = true; for (uint32_t t : jt) if (in_fn(t)) work.push(t); } else { unresolved_indirect = true; // computed jump we can't bound } } break; } } } ExtentP3 info; info.terminus = visited.empty() ? entry : (*visited.rbegin() + 4u); // Density guard: a real function's basic blocks densely cover // [entry, terminus]. A walk that follows a data-decoded branch/jump into // an inline data region can coincidentally reach a far jr $ra (the word // 0x03E00008 appears in data) and look "clean" while spanning tens of KB // it barely visited — that over-extended range then hosts many aliases // and emits the whole data region as code (huge bloat). Require the // visited instructions to cover a meaningful fraction (~25%) of the span // so sparse data-following walks are rejected and fall back to the // density heuristic. Real functions (incl. those with small inline jump // tables) stay well above this floor. uint32_t span = info.terminus - entry; bool dense = (span == 0) || (static_cast(visited.size()) * 16u >= span); info.clean_code = reached_exit && !hit_invalid && !unresolved_indirect && !visited.empty() && dense; info.hit_invalid = hit_invalid; info.resolved_jt = resolved_jt; info.unresolved_indirect = unresolved_indirect; return info; }; for (size_t i = 0; i < starts_vec.size(); i++) { Function func; func.start_addr = starts_vec[i]; uint32_t next_start = (i + 1 < starts_vec.size()) ? starts_vec[i + 1] : end_addr; ExtentP3 ext = compute_extent_p3(func.start_addr, next_start); if (ext.clean_code) { // Real function: bound to its control-flow terminus so trailing // inline data / jump tables are excluded from the emitted extent. func.end_addr = ext.terminus; if (func.end_addr <= func.start_addr) func.end_addr = func.start_addr + 4u; if (func.end_addr > next_start) func.end_addr = next_start; func.is_data_section = false; } else if (ext.hit_invalid && !ext.resolved_jt) { // The reachable body (reached via straight/branch flow, no resolved // jump table) decodes with an invalid primary opcode — real PS1 code // never executes an invalid opcode, so this "function start" is // genuinely data (e.g. a filename/string or pointer-table slot minted // as a start by pointer-table promotion). Classify as data even when // the dense-minted-start span is below is_likely_data_section's size // floor. Guarded by !resolved_jt so a mis-resolved jump table can // never force a real function to a stub. func.end_addr = next_start; func.is_data_section = true; } else { // Legacy boundary estimate + density heuristic, but bound end_addr to // the control-flow walk terminus (a valid upper bound on code // reachable from the entry). Without this, a function-start on a // data-heavy tail with no intervening discovered entry gets // end_addr = next_start spanning tens of KB of inline data; that // over-extended range then hosts many pointer-table aliases and emits // the whole data region as code (massive bloat). The terminus bound // is skipped only when an unresolved indirect jump means real code may // exist beyond the walk that we cannot safely bound. func.end_addr = next_start; auto ret_it = function_last_return.find(func.start_addr); if (ret_it != function_last_return.end()) { uint32_t return_end = ret_it->second + 8u; /* include jr delay slot */ if (return_end > func.start_addr && return_end < func.end_addr) { func.end_addr = return_end; } } if (!ext.unresolved_indirect && ext.terminus > func.start_addr && ext.terminus < func.end_addr) { func.end_addr = ext.terminus; } func.is_data_section = is_likely_data_section(func.start_addr, func.end_addr); } func.size = func.end_addr - func.start_addr; func.name = fmt::format("func_{:08X}", func.start_addr); // Check for prologue at start auto first_instr = exe_.read_word(func.start_addr); if (first_instr.has_value()) { int32_t stack_size; func.has_prologue = is_prologue(*first_instr, stack_size); func.stack_frame_size = func.has_prologue ? stack_size : 0; } else { func.has_prologue = false; func.stack_frame_size = 0; } // Check for jr $ra before end func.has_epilogue = false; uint32_t search_end = func.end_addr - 4; for (uint32_t addr = func.start_addr; addr <= search_end && addr < func.end_addr; addr += 4) { auto instr_opt = exe_.read_word(addr); if (instr_opt.has_value() && is_jr_ra(*instr_opt)) { func.has_epilogue = true; break; } } result.functions.push_back(func); } return result; } } // namespace PSXRecomp