/** * @file codegen/function_scanner.cpp * * @copyright Copyright (c) 2026 Tom Clay * All rights reserved. * * @license BSD 3-Clause License * See LICENSE file in the project root for full license text. */ #include "codegen_flags.h" #include "decoded_binary.h" #include "ppc/instruction.h" #include "ppc/opcode.h" #include #include #include #include #include #include #include #include #include #include #include "codegen_logging.h" #include #include namespace rex::codegen { // Import PPC types using rex::codegen::ppc::decode_instruction; using rex::codegen::ppc::Instruction; using rex::codegen::ppc::Opcode; using rex::memory::load_and_swap; //============================================================================= // Constructors //============================================================================= FunctionScanner::FunctionScanner(const BinaryView& binary) : binary_(&binary) {} //============================================================================= // Address Translation //============================================================================= template const T* FunctionScanner::translate_address(rex::guest_addr_t guest_addr) const { return reinterpret_cast(binary_->translate(static_cast(guest_addr))); } bool FunctionScanner::isExecutableSection(rex::guest_addr_t address) const { return binary_->isExecutable(static_cast(address)); } //============================================================================= // Helper: Detect function prologue pattern //============================================================================= bool FunctionScanner::is_prologue_pattern(guest_addr_t address) { auto host_ptr = translate_address(address); if (!host_ptr) return false; u32 code = load_and_swap(host_ptr); Instruction instr = decode_instruction(address, code); // Check for mflr if (instr.opcode == Opcode::mflr) { return true; } // Check for mfspr lr (SPR 8) if (instr.opcode == Opcode::mfspr && instr.XFX.spr_num() == 8) { return true; } // Check for stack frame allocation: stwu r1, -X(r1) if (instr.opcode == Opcode::stwu && instr.D.RS() == 1 && instr.D.RA == 1 && instr.D.SIMM() < 0) { return true; } return false; } //============================================================================= // Helper: Detect function epilogue pattern //============================================================================= bool FunctionScanner::is_epilogue_pattern(guest_addr_t address) { auto host_ptr = translate_address(address); if (!host_ptr) return false; u32 code = load_and_swap(host_ptr); Instruction instr = decode_instruction(address, code); // Check for blr if (instr.is_return()) { return true; } // Check for mtlr if (instr.opcode == Opcode::mtlr) { return true; } // Check for stack restore: lwz r1, 0(r1) if (instr.opcode == Opcode::lwz && instr.D.RT == 1 && instr.D.RA == 1 && instr.D.SIMM() == 0) { return true; } return false; } //============================================================================= // Helper: Detect save/restore helper functions via byte pattern matching //============================================================================= bool FunctionScanner::is_restgprlr_function(guest_addr_t address) { auto host_ptr = translate_address(address); if (!host_ptr) return false; u32 first = load_and_swap(host_ptr); // Check single-instruction patterns (4 bytes each) constexpr u32 RESTGPRLR_14 = 0xe9c1ff68; // ld r14, -0x98(r1) constexpr u32 SAVEGPRLR_14 = 0xf9c1ff68; // std r14, -0x98(r1) constexpr u32 RESTFPR_14 = 0xc9ccff70; // lfd f14, -0x90(r12) constexpr u32 SAVEFPR_14 = 0xd9ccff70; // stfd f14, -0x90(r12) if (first == RESTGPRLR_14 || first == SAVEGPRLR_14 || first == RESTFPR_14 || first == SAVEFPR_14) { return true; } // Check two-instruction patterns (8 bytes each) // Pattern: li r11, -0x120 (0x3960fee0) + lvx/stvx if (first == 0x3960fee0) { // li r11, -0x120 auto second_ptr = translate_address(address + 4); if (second_ptr) { u32 second = load_and_swap(second_ptr); constexpr u32 RESTVMX_14 = 0x7dcb60ce; // lvx v14, r11, r12 constexpr u32 SAVEVMX_14 = 0x7dcb61ce; // stvx v14, r11, r12 if (second == RESTVMX_14 || second == SAVEVMX_14) { return true; } } } // Pattern: li r11, -0x400 (0x3960fc00) + lvx128/stvx128 if (first == 0x3960fc00) { // li r11, -0x400 auto second_ptr = translate_address(address + 4); if (second_ptr) { u32 second = load_and_swap(second_ptr); constexpr u32 RESTVMX_64 = 0x100b60cb; // lvx128 v64, r11, r12 constexpr u32 SAVEVMX_64 = 0x100b61cb; // stvx128 v64, r11, r12 if (second == RESTVMX_64 || second == SAVEVMX_64) { return true; } } } return false; } //============================================================================= // Jump Table Pattern Detection //============================================================================= // Xbox 360 compilers emit 4 distinct jump table patterns (maybe more?): // // 1. ABSOLUTE: lwzx loads full 32-bit target addresses // lis rT, table@ha; addi rT, rT, table@l; rlwinm rI, rIdx, 2; lwzx rT, rI, rT; mtctr; bctr // // 2. COMPUTED: lbzx loads byte offset, shifted and added to base // lis rT, table@ha; addi rT, rT, table@l; lbzx rO, rIdx, rT; rlwinm rO, rO, shift; // lis rB, base@ha; addi rB, rB, base@l; add rT, rB, rO; mtctr; bctr // // 3. BYTEOFFSET: lbzx loads byte offset, added directly to base // lis rT, table@ha; addi rT, rT, table@l; lbzx rO, rIdx, rT; // lis rB, base@ha; addi rB, rB, base@l; add rT, rB, rO; mtctr; bctr // // 4. SHORTOFFSET: lhzx loads 16-bit offset, added to base // lis rT, table@ha; addi rT, rT, table@l; rlwinm rI, rIdx, 1; lhzx rO, rI, rT; // lis rB, base@ha; addi rB, rB, base@l; add rT, rB, rO; mtctr; bctr //============================================================================= namespace { // Jump table type - internal use only during detection // Determines how target addresses are stored/computed in the binary enum class JumpTableType : u8 { kAbsolute, // lwzx - table contains full 32-bit target addresses kComputed, // lbzx + rlwinm + add - byte offset shifted and added to base kByteOffset, // lbzx + add - byte offset added directly to base kShortOffset, // lhzx + add - 16-bit offset added to base }; // Helper struct for tracking pattern match state struct JumpTableMatch { JumpTableType type = JumpTableType::kAbsolute; u8 ctr_source_reg = 0; // Register moved to CTR u8 table_reg = 0; // Register holding table address u8 base_reg = 0; // Register holding base address (offset types) u8 index_reg = 0; // Register holding original switch index u8 offset_reg = 0; // Register holding loaded offset u8 shift_amount = 0; // Shift amount for COMPUTED type guest_addr_t table_high = 0; guest_addr_t table_low = 0; guest_addr_t base_high = 0; guest_addr_t base_low = 0; // Pattern matching state bool found_mtctr = false; bool found_add = false; // For offset-based types bool found_load = false; // lwzx/lbzx/lhzx bool found_shift = false; // rlwinm for shift bool found_table_lis = false; bool found_table_addi = false; bool found_base_lis = false; bool found_base_addi = false; // For @ha/@l pairs, addi uses signed immediate, so we need addition with sign extension guest_addr_t table_address() const { return table_high + static_cast(table_low); } guest_addr_t base_address() const { return base_high + static_cast(base_low); } }; // Scan backward for CMPLWI bounds check and conditional branch struct BoundsInfo { uint32_t maxEntries = 0; uint8_t indexReg = 0; uint32_t defaultTarget = 0; bool found = false; }; //============================================================================= // Helper: Scan for bounds check (CMPLWI + BGT/BLE) //============================================================================= BoundsInfo scanForBounds(const FunctionScanner& scanner, guest_addr_t bctr_address, u8 expected_index_reg) { BoundsInfo bounds; constexpr int MAX_SCAN = 64; u8 cr_field = 0xFF; // CR field from conditional branch for (int i = 1; i <= MAX_SCAN; ++i) { guest_addr_t addr = bctr_address - (i * 4); if (addr < 4) break; auto host_ptr = scanner.translate_address(addr); if (!host_ptr) break; u32 code = load_and_swap(host_ptr); if (code == 0) break; Instruction instr = decode_instruction(addr, code); // Look for conditional branch (bc, bca, bcl, bcla, bclr, bclrl) // bgt/ble/bgtlr/blelr are simplified mnemonics for bc with specific BO/BI if (cr_field == 0xFF) { bool is_cond_branch = (instr.opcode == Opcode::bc || instr.opcode == Opcode::bca || instr.opcode == Opcode::bcl || instr.opcode == Opcode::bcla || instr.opcode == Opcode::bclr || instr.opcode == Opcode::bclrl); if (is_cond_branch) { // Check if this could be a bounds check branch (bgt or ble pattern) // BO[4] (bit 0) = 0 means test CR bit // BI[0:1] = condition bit within CR field (GT=1, LT=0, EQ=2, SO=3) u8 bi = instr.B.BI; // Check for branches that test the GT bit (BI mod 4 == 1) // bgt: BO=12 (01100), tests CR[GT]=true // ble: BO=4 (00100), tests CR[GT]=false (i.e., not greater = less or equal) if ((bi & 0x3) == 1) { // Tests GT bit // Extract CR field (bits 2-4 of BI) cr_field = (bi >> 2) & 0x7; // Extract default target if branch has target if (instr.branch_target.has_value()) { bounds.defaultTarget = instr.branch_target.value(); } } } } // Look for rlwinm that sets the index register with a mask // clrlwi rD, rS, n = rlwinm rD, rS, 0, n, 31 // This bounds the value to 2^(32-n) - 1 entries if (instr.opcode == Opcode::rlwinm && instr.M.RA == expected_index_reg) { u8 sh = instr.M.SH; u8 mb = instr.M.MB; u8 me = instr.M.ME; // clrlwi pattern: SH=0, ME=31, MB > 0 // The mask clears leftmost MB bits, so max value = 2^(32-MB) - 1 // Entry count = max value + 1 = 2^(32-MB) if (sh == 0 && me == 31 && mb > 0 && mb < 32) { u32 implicit_count = 1u << (32 - mb); REXCODEGEN_TRACE( " [0x{:08X}] Found clrlwi/rlwinm r{}, ..., {} -> {} entries (implicit mask)", addr, static_cast(expected_index_reg), mb, implicit_count); // Only accept if count is reasonable (2-256 entries) if (implicit_count >= 2 && implicit_count <= 256) { bounds.maxEntries = implicit_count; bounds.indexReg = expected_index_reg; bounds.found = true; break; // Implicit bounds are definitive } } } // Look for cmpli or cmpi (cmplwi/cmpwi - unsigned/signed bounds check) if (instr.opcode == Opcode::cmpli || instr.opcode == Opcode::cmpi) { // cmpli format: cmpli BF, L, RA, UIMM // BF (bits 23-25): CR field // L (bit 21): 0=32-bit, 1=64-bit // RA (bits 16-20): Register to compare // UIMM (bits 0-15): Immediate value // BF is top 3 bits of RT field for compare instructions u8 cmp_cr = instr.D.RT >> 2; u8 cmp_ra = instr.D.RA; // Register being compared u16 cmp_imm = instr.D.UIMM(); // Immediate value (max index) // Prefer register match, accept CR-only match as fallback bool cr_matches = (cr_field != 0xFF && cmp_cr == cr_field); bool reg_matches = (cmp_ra == expected_index_reg); // CRITICAL: Reject very small immediates (0 or 1) even if register matches // These are likely unrelated comparisons (checking for zero/null, boolean tests) // A real switch table bounds check would have immediate >= 2 (at least 3 cases) if (cmp_imm <= 1) { REXCODEGEN_TRACE( " [0x{:08X}] Skipping cmpli r{}, {} (immediate too small for switch bounds)", addr, static_cast(cmp_ra), cmp_imm); continue; } // Only accept if register matches, or if CR matches AND immediate is reasonable if (reg_matches || (cr_matches && cmp_imm > 1)) { bounds.maxEntries = cmp_imm + 1; // cmpli compares against max, so count = max + 1 bounds.indexReg = cmp_ra; bounds.found = true; // If register matches, we found the best match - done if (reg_matches) break; // If only CR matches, continue scanning for a better (register) match } } } return bounds; } //============================================================================= // Helper: Read table entries based on type //============================================================================= std::vector read_table_entries(const FunctionScanner& scanner, const JumpTableMatch& match, u32 entry_count) { std::vector targets; guest_addr_t table_addr = match.table_address(); // entry_count comes from bounds check analysis - use it exactly if available // If no bounds check was found (entry_count == 0), read until we hit invalid entries // Loop terminates via goto done when invalid memory is hit for (u32 i = 0; entry_count == 0 || i < entry_count; ++i) { guest_addr_t target = 0; switch (match.type) { case JumpTableType::kAbsolute: { // Read 32-bit address directly (big-endian) auto entry_ptr = scanner.translate_address(table_addr + (i * 4)); if (!entry_ptr) goto done; target = load_and_swap(entry_ptr); break; } case JumpTableType::kComputed: { // Read byte, shift, add to base auto entry_ptr = scanner.translate_address(table_addr + i); if (!entry_ptr) goto done; u8 offset = *entry_ptr; target = match.base_address() + (static_cast(offset) << match.shift_amount); break; } case JumpTableType::kByteOffset: { // Read byte, add directly to base auto entry_ptr = scanner.translate_address(table_addr + i); if (!entry_ptr) goto done; u8 offset = *entry_ptr; target = match.base_address() + offset; break; } case JumpTableType::kShortOffset: { // Read 16-bit value (big-endian), add to base auto entry_ptr = scanner.translate_address(table_addr + (i * 2)); if (!entry_ptr) goto done; u16 offset = load_and_swap(entry_ptr); target = match.base_address() + offset; break; } } // PPC instructions must be 4-byte aligned if (target & 3) goto done; // Validate target (stop on null or invalid for absolute type) if (target == 0 && match.type == JumpTableType::kAbsolute) goto done; // Validate target is in EXECUTABLE section (not just mapped memory) // This prevents false positives from data tables containing addresses if (!scanner.isExecutableSection(target)) { // For absolute tables, non-executable target means wrong table address if (match.type == JumpTableType::kAbsolute) goto done; continue; // Skip non-executable entries in offset tables (might be default/error cases) } targets.push_back(target); } done: return targets; } } // anonymous namespace //============================================================================= // Helper: Check if instruction indicates a function boundary //============================================================================= // Returns true if the instruction at 'code' indicates we've crossed into // a different function (either previous function's terminator or current // function's prologue). static bool is_function_boundary(u32 code, const Instruction& instr, guest_addr_t addr) { // Zero padding between functions if (code == 0x00000000) { REXCODEGEN_TRACE(" [0x{:08X}] Hit zero padding - function boundary", addr); return true; } // blr - previous function's return if (instr.is_return()) { REXCODEGEN_TRACE(" [0x{:08X}] Hit blr - function boundary", addr); return true; } // bctr/bctrl - indirect branch/call via CTR if (instr.opcode == Opcode::bcctr || instr.opcode == Opcode::bcctrl) { REXCODEGEN_TRACE(" [0x{:08X}] Hit bctr/bctrl - function boundary", addr); return true; } // Unconditional branch 'b' (tail call to named function) if (instr.opcode == Opcode::b || instr.opcode == Opcode::ba) { REXCODEGEN_TRACE(" [0x{:08X}] Hit unconditional branch (b) - function boundary", addr); return true; } // mflr - function prologue (saving link register) if (instr.opcode == Opcode::mflr) { REXCODEGEN_TRACE(" [0x{:08X}] Hit mflr - function prologue", addr); return true; } // stw rX, offset(r1) where offset is negative - stack frame setup // This catches "stwu r1, -N(r1)" which is a common prologue if (instr.opcode == Opcode::stwu && instr.D.RA == 1 && instr.D.RT == 1) { // stwu r1, -N(r1) is stack frame allocation REXCODEGEN_TRACE(" [0x{:08X}] Hit stwu r1 (stack frame) - function prologue", addr); return true; } // nop (ori r0,r0,0 = 0x60000000) - often used as padding // But nops can appear mid-function too, so only treat consecutive nops as boundary // For now, don't treat single nop as boundary return false; } //============================================================================= // Helper: Detect jump table pattern at bctr instruction //============================================================================= std::optional FunctionScanner::detect_jump_table(guest_addr_t bctr_address) { // Skip detection if this address has a manually-specified switch table if (known_switch_tables_.count(static_cast(bctr_address))) { REXCODEGEN_TRACE("detect_jump_table: skipping 0x{:08X} (manual table exists)", bctr_address); return std::nullopt; // Will be handled by the pre-loaded config } const int MAX_SCAN_BACK = static_cast(REXCVAR_GET(backward_scan_limit)); JumpTableMatch match; REXCODEGEN_TRACE("detect_jump_table: scanning backward from bctr at 0x{:08X}", bctr_address); // Backward scan to match pattern for (int i = 1; i <= MAX_SCAN_BACK; ++i) { guest_addr_t addr = bctr_address - (i * 4); if (addr < 4) break; auto host_ptr = translate_address(addr); if (!host_ptr) break; // Read as big-endian (PPC is big-endian) u32 code = load_and_swap(host_ptr); Instruction instr = decode_instruction(addr, code); // Stop at function boundaries - but allow continuing past bctr if we're still // looking for lis (handles adjacent switch tables sharing setup code) if (is_function_boundary(code, instr, addr)) { // If we found load but not lis, and this is bctr, continue scanning // (adjacent switch tables may share the same lis setup) if (instr.opcode == Opcode::bcctr && match.found_load && !match.found_table_lis) { REXCODEGEN_TRACE(" [0x{:08X}] Continuing past bctr to find shared lis", addr); continue; } break; } // Step 1: Find mtctr rX if (!match.found_mtctr && instr.opcode == Opcode::mtctr) { match.ctr_source_reg = instr.XFX.RS(); match.found_mtctr = true; REXCODEGEN_TRACE(" [0x{:08X}] Found mtctr r{}", addr, static_cast(match.ctr_source_reg)); continue; } if (!match.found_mtctr) continue; // Step 2a: Find add rT, rBase, rOffset (for offset-based types) if (!match.found_add && !match.found_load && instr.opcode == Opcode::add) { if (instr.XO.RT == match.ctr_source_reg) { // Store both RA and RB - we'll determine which is base/offset later match.base_reg = instr.XO.RA; match.offset_reg = instr.XO.RB; match.found_add = true; REXCODEGEN_TRACE(" [0x{:08X}] Found add r{}, r{}, r{}", addr, static_cast(instr.XO.RT), static_cast(match.base_reg), static_cast(match.offset_reg)); continue; } } // Step 2b: Find lwzx (ABSOLUTE type) - only if no add found // lwzx RT, RA, RB: RT = mem[RA + RB] // Note: RA and RB can be in either order (table/index or index/table) if (!match.found_add && !match.found_load && instr.opcode == Opcode::lwzx) { if (instr.X.RT == match.ctr_source_reg) { match.type = JumpTableType::kAbsolute; // Initially assume RA=table, RB=index (will verify/swap later) match.table_reg = instr.X.RA; match.index_reg = instr.X.RB; match.found_load = true; REXCODEGEN_TRACE(" [0x{:08X}] Found lwzx r{}, r{}, r{} (tentative table=r{}, index=r{})", addr, static_cast(instr.X.RT), static_cast(instr.X.RA), static_cast(instr.X.RB), static_cast(match.table_reg), static_cast(match.index_reg)); continue; } } // For offset-based types: find the load instruction if (match.found_add && !match.found_load) { // lbzx for COMPUTED or BYTEOFFSET // lbzx RT, RA, RB: RT = *(RA + RB) if (instr.opcode == Opcode::lbzx && instr.X.RT == match.offset_reg) { // Initially assume RA=table, RB=index match.table_reg = instr.X.RA; match.index_reg = instr.X.RB; match.found_load = true; // Only set type if we haven't already found a shift (which means kComputed) if (!match.found_shift) { match.type = JumpTableType::kByteOffset; // Default when no shift } REXCODEGEN_TRACE(" [0x{:08X}] Found lbzx r{}, r{}, r{}", addr, static_cast(instr.X.RT), static_cast(instr.X.RA), static_cast(instr.X.RB)); continue; } // lhzx for SHORTOFFSET if (instr.opcode == Opcode::lhzx && instr.X.RT == match.offset_reg) { match.type = JumpTableType::kShortOffset; match.table_reg = instr.X.RA; match.index_reg = instr.X.RB; match.found_load = true; REXCODEGEN_TRACE(" [0x{:08X}] Found lhzx r{}, r{}, r{}", addr, static_cast(instr.X.RT), static_cast(instr.X.RA), static_cast(instr.X.RB)); continue; } } // Step 3: Find rlwinm for shift (COMPUTED type or index scaling) if (!match.found_shift && instr.opcode == Opcode::rlwinm) { // Check if this is scaling the index (for ABSOLUTE, SHORT, or offset-based types) // Must check BEFORE offset shift to handle SHORTOFFSET with scaled index if (match.found_load && instr.M.RA == match.index_reg) { match.index_reg = instr.M.RS; match.found_shift = true; REXCODEGEN_TRACE(" [0x{:08X}] Found rlwinm (index scale) r{}, r{}, {}", addr, static_cast(instr.M.RA), static_cast(instr.M.RS), static_cast(instr.M.SH)); continue; } // Check if this is shifting the offset (COMPUTED type) // Only set COMPUTED type if we haven't already identified as SHORTOFFSET // (SHORTOFFSET uses rlwinm for index scaling, not offset shifting) if (match.found_add && instr.M.RA == match.offset_reg && match.type != JumpTableType::kShortOffset) { match.shift_amount = instr.M.SH; match.type = JumpTableType::kComputed; match.found_shift = true; match.offset_reg = instr.M.RS; REXCODEGEN_TRACE(" [0x{:08X}] Found rlwinm (shift) r{}, r{}, {} -> COMPUTED type", addr, static_cast(instr.M.RA), static_cast(instr.M.RS), static_cast(instr.M.SH)); continue; } } // Step 4: Find lis/addi pairs for table address (and base for register reuse case) // Also check if we need to swap table_reg/index_reg (RA/RB ambiguity) if (match.found_load) { // Check for register reuse: same register used for both table and base (offset-based types) // In this case, backward scan finds BASE first (closer to bctr), then TABLE bool register_reuse = match.found_add && (match.table_reg == match.base_reg); // Check for lis matching table_reg if (instr.opcode == Opcode::lis && instr.D.RT == match.table_reg) { if (register_reuse) { // Register reuse: first lis = BASE, second lis = TABLE if (!match.found_base_lis) { match.base_high = static_cast(instr.D.UIMM()) << 16; match.found_base_lis = true; REXCODEGEN_TRACE(" [0x{:08X}] Found base lis r{}, 0x{:04X} (register reuse)", addr, static_cast(instr.D.RT), instr.D.UIMM()); continue; } else if (!match.found_table_lis) { match.table_high = static_cast(instr.D.UIMM()) << 16; match.found_table_lis = true; REXCODEGEN_TRACE(" [0x{:08X}] Found table lis r{}, 0x{:04X} (register reuse)", addr, static_cast(instr.D.RT), instr.D.UIMM()); continue; } } else if (!match.found_table_lis) { match.table_high = static_cast(instr.D.UIMM()) << 16; match.found_table_lis = true; REXCODEGEN_TRACE(" [0x{:08X}] Found lis r{}, 0x{:04X}", addr, static_cast(instr.D.RT), instr.D.UIMM()); continue; } } // Check if lis matches index_reg - means we guessed wrong, need to swap else if (!match.found_table_lis && instr.opcode == Opcode::lis && instr.D.RT == match.index_reg) { REXCODEGEN_TRACE(" [0x{:08X}] Found lis for index_reg r{}, swapping table/index", addr, static_cast(instr.D.RT)); std::swap(match.table_reg, match.index_reg); match.table_high = static_cast(instr.D.UIMM()) << 16; match.found_table_lis = true; continue; } // Check for addi/ori matching table_reg if ((instr.opcode == Opcode::addi || instr.opcode == Opcode::ori) && instr.D.RT == match.table_reg) { if (register_reuse) { // Register reuse: first addi = BASE, second addi = TABLE if (!match.found_base_addi) { match.base_low = instr.D.UIMM(); match.found_base_addi = true; REXCODEGEN_TRACE(" [0x{:08X}] Found base addi r{}, 0x{:04X} (register reuse)", addr, static_cast(instr.D.RT), instr.D.UIMM()); continue; } else if (!match.found_table_addi) { match.table_low = instr.D.UIMM(); match.found_table_addi = true; REXCODEGEN_TRACE(" [0x{:08X}] Found table addi r{}, 0x{:04X} (register reuse)", addr, static_cast(instr.D.RT), instr.D.UIMM()); continue; } } else if (!match.found_table_addi) { match.table_low = instr.D.UIMM(); match.found_table_addi = true; REXCODEGEN_TRACE(" [0x{:08X}] Found addi r{}, r{}, 0x{:04X}", addr, static_cast(instr.D.RT), static_cast(instr.D.RA), instr.D.UIMM()); continue; } } // Check if addi matches index_reg - means we guessed wrong, need to swap else if (!match.found_table_addi && (instr.opcode == Opcode::addi || instr.opcode == Opcode::ori) && instr.D.RT == match.index_reg) { REXCODEGEN_TRACE(" [0x{:08X}] Found addi for index_reg r{}, swapping table/index", addr, static_cast(instr.D.RT)); std::swap(match.table_reg, match.index_reg); match.table_low = instr.D.UIMM(); match.found_table_addi = true; continue; } } // Step 5: Find lis/addi pairs for base address (offset-based types) // Also handle RA/RB ambiguity for add instruction if (match.found_add) { if (!match.found_base_lis && instr.opcode == Opcode::lis) { if (instr.D.RT == match.base_reg) { match.base_high = static_cast(instr.D.UIMM()) << 16; match.found_base_lis = true; REXCODEGEN_TRACE(" [0x{:08X}] Found base lis r{}, 0x{:04X}", addr, static_cast(instr.D.RT), instr.D.UIMM()); continue; } // Check if lis matches offset_reg - means we guessed wrong else if (instr.D.RT == match.offset_reg && !match.found_base_lis) { REXCODEGEN_TRACE(" [0x{:08X}] Found lis for offset_reg r{}, swapping base/offset", addr, static_cast(instr.D.RT)); std::swap(match.base_reg, match.offset_reg); match.base_high = static_cast(instr.D.UIMM()) << 16; match.found_base_lis = true; continue; } } if (!match.found_base_addi && (instr.opcode == Opcode::addi || instr.opcode == Opcode::ori)) { if (instr.D.RT == match.base_reg) { match.base_low = instr.D.UIMM(); match.found_base_addi = true; REXCODEGEN_TRACE(" [0x{:08X}] Found base addi r{}, 0x{:04X}", addr, static_cast(instr.D.RT), instr.D.UIMM()); continue; } // Check if addi matches offset_reg - means we guessed wrong else if (instr.D.RT == match.offset_reg && !match.found_base_addi) { REXCODEGEN_TRACE(" [0x{:08X}] Found addi for offset_reg r{}, swapping base/offset", addr, static_cast(instr.D.RT)); std::swap(match.base_reg, match.offset_reg); match.base_low = instr.D.UIMM(); match.found_base_addi = true; continue; } } } // Check if we have a complete pattern bool table_complete = match.found_table_lis && match.found_table_addi; bool base_complete = !match.found_add || (match.found_base_lis && match.found_base_addi); if (match.found_mtctr && match.found_load && table_complete && base_complete) { REXCODEGEN_TRACE(" Pattern complete at 0x{:08X}", addr); break; // Pattern complete } } // Verify minimum required pattern elements if (!match.found_mtctr || !match.found_load || !match.found_table_lis || !match.found_table_addi) { REXCODEGEN_TRACE(" Pattern incomplete: mtctr={}, load={}, table_lis={}, table_addi={}", match.found_mtctr, match.found_load, match.found_table_lis, match.found_table_addi); // Only report error if we found indexed load AND lis/addi for the table address. // If we found load but NO lis, it's a vtable/indirect call (runtime pointer), not a switch // table. if (match.found_load && (match.found_table_lis || match.found_base_lis)) { REXCODEGEN_ERROR( "Jump table detection failed at bctr 0x{:08X}: mtctr={}, load={}, table_lis={}, " "table_addi={}, table_reg=r{}, base_lis={}, base_addi={}", bctr_address, match.found_mtctr, match.found_load, match.found_table_lis, match.found_table_addi, match.table_reg, match.found_base_lis, match.found_base_addi); } else if (match.found_load) { // Load but no lis = vtable/indirect call with runtime pointer, not a switch table REXCODEGEN_TRACE("bctr 0x{:08X}: indexed load without lis - treating as vtable/indirect call", bctr_address); } return std::nullopt; } // For offset-based types, require base address if (match.found_add && (!match.found_base_lis || !match.found_base_addi)) { REXCODEGEN_TRACE(" Offset-based pattern incomplete: base_lis={}, base_addi={}", match.found_base_lis, match.found_base_addi); return std::nullopt; } // Validate table address guest_addr_t table_address = match.table_address(); REXCODEGEN_TRACE(" Table address: 0x{:08X} (high=0x{:08X}, low=0x{:04X})", table_address, match.table_high, match.table_low); auto table_ptr = translate_address(table_address); if (!table_ptr) { REXCODEGEN_TRACE(" Invalid table address 0x{:08X} - not in mapped memory", table_address); return std::nullopt; } // Scan for bounds check (CMPLWI) BoundsInfo bounds = scanForBounds(*this, bctr_address, match.index_reg); REXCODEGEN_TRACE(" Bounds check: found={}, count={}, default=0x{:08X}, index_reg=r{}", bounds.found, bounds.maxEntries, bounds.defaultTarget, bounds.indexReg); // Read table entries std::vector targets = read_table_entries(*this, match, bounds.maxEntries); // Require at least 2 entries if (targets.size() < 2) { REXCODEGEN_TRACE(" Insufficient entries: {} (need at least 2)", targets.size()); return std::nullopt; } // Build result JumpTable jt; jt.bctrAddress = static_cast(bctr_address); jt.tableAddress = static_cast(table_address); jt.indexRegister = match.index_reg; jt.targets = std::move(targets); return jt; } //============================================================================= // Block-Based Discovery //============================================================================= FunctionBlocks FunctionScanner::discover_blocks(rex::guest_addr_t entry_point, rex::u32 pdata_size) { FunctionBlocks result; result.entry = entry_point; result.pdata_size = pdata_size; // Track all instruction addresses scanned (prevents overlap between blocks) std::unordered_set scannedAddrs; // DFS block stack - tracks blocks being processed // this allows projection carry-forward on continuous blocks std::vector block_stack; // Start with entry block DiscoveredBlock entry_block; entry_block.base = entry_point; entry_block.end = entry_point; entry_block.projectedSize = -1; // No limit block_stack.push_back(entry_block); const size_t MAX_BLOCKS = REXCVAR_GET(max_blocks_per_function); // Safety limit while (!block_stack.empty() && result.blocks.size() < MAX_BLOCKS) { // Get current block from stack (by reference for in-place modification) DiscoveredBlock& block = block_stack.back(); // Only check for duplicates if this is a FRESH block (not partially scanned) // When block.end > block.base, we're continuing to process an existing block if (block.end == block.base) { // Fresh block - check if already scanned by another block if (scannedAddrs.count(block.base)) { block_stack.pop_back(); continue; } } // Validate alignment if ((block.base & 0x3) != 0) { REXCODEGEN_WARN("discover_blocks: misaligned block start 0x{:08X}", block.base); block_stack.pop_back(); continue; } // Calculate current position in block guest_addr_t addr = block.end; // end tracks where we are (exclusive becomes next addr) if (addr == block.base) { // Fresh block - start scanning from base } // Check projection limit BEFORE processing instruction // if block.size >= projectedSize, block is done guest_addr_t block_size = addr - block.base; if (block.projectedSize != -1 && block_size >= static_cast(block.projectedSize)) { REXCODEGEN_TRACE("Block 0x{:08X} hit projection limit at size 0x{:X}", block.base, block_size); // Block done - save and pop result.blocks.push_back(block); block_stack.pop_back(); continue; } // Check if this address was already scanned by another block (overlap prevention) // This catches cases where a fall-through block tries to scan into territory // already covered by another block (e.g., shared epilogue code) if (scannedAddrs.count(addr)) { // Block ends here - don't include the already-scanned instruction if (addr > block.base) { // Record the overlap address as a successor so codegen emits a goto block.successors.push_back(addr); block.has_terminator = true; result.blocks.push_back(block); } block_stack.pop_back(); continue; } // CRITICAL: Check if we've hit another function's entry point // This enforces the authority system - PDATA/config entries cannot be consumed if (addr != entry_point && known_callables_.contains(static_cast(addr))) { REXCODEGEN_TRACE("discover_blocks: hit entry point 0x{:08X} - stopping block", addr); if (addr > block.base) { // Block has content - save it block.end = addr; // Don't include this instruction block.has_terminator = true; result.blocks.push_back(block); } // Either way, pop this block - we can't continue into another function block_stack.pop_back(); continue; } // Fetch instruction auto host_ptr = translate_address(addr); if (!host_ptr) { REXCODEGEN_DEBUG("discover_blocks: invalid address 0x{:08X}", addr); block.has_terminator = true; result.blocks.push_back(block); block_stack.pop_back(); continue; } u32 code = load_and_swap(host_ptr); // Null instruction ends block if (code == 0x00000000) { block.end = addr; // Don't include the null block.has_terminator = true; result.blocks.push_back(block); block_stack.pop_back(); continue; } // Include this instruction in block block.end = addr + 4; scannedAddrs.insert(addr); // Mark this address as scanned Instruction instr = decode_instruction(addr, code); // Check for blr (return) if (instr.is_return()) { block.has_terminator = true; result.blocks.push_back(block); block_stack.pop_back(); continue; } // Check for bctr (indirect branch) if (instr.opcode == Opcode::bcctr) { auto jt_info = detect_jump_table(addr); if (jt_info.has_value()) { result.jump_tables.push_back(jt_info.value()); // Add all jump table targets as successors for (guest_addr_t target : jt_info->targets) { block.successors.push_back(target); } } block.has_terminator = true; result.blocks.push_back(block); block_stack.pop_back(); // Push jump table targets onto stack (if any) if (jt_info.has_value()) { for (guest_addr_t target : jt_info->targets) { if (!scannedAddrs.count(target)) { DiscoveredBlock jt_block; jt_block.base = target; jt_block.end = target; jt_block.projectedSize = -1; block_stack.push_back(jt_block); } } } continue; } // Check for unconditional branch (b/ba) if (instr.opcode == Opcode::b || instr.opcode == Opcode::ba) { if (instr.branch_target.has_value()) { guest_addr_t target = instr.branch_target.value(); block.successors.push_back(target); // Check known_callables_ FIRST (gathered before discovery) bool is_tail_call = known_callables_.contains(static_cast(target)); // Backward branch to unknown = probably tail call if (!is_tail_call && target < entry_point) { is_tail_call = true; } // Large forward branch (>1MB) = probably tail call to shared code // No legitimate internal branch spans more than 1MB if (!is_tail_call && target > addr && (target - addr) > 0x100000) { is_tail_call = true; } // Check if target is a known callable (function or import) if (!is_tail_call && isKnownCallable(static_cast(target))) { is_tail_call = true; } // CRITICAL: Check code region boundary // If target is in a different region (and not a configured chunk), // it MUST be a tail call - prevents mega-merges across null boundaries if (!is_tail_call && !isInternalBranch(static_cast(addr), static_cast(target), static_cast(entry_point))) { is_tail_call = true; } // CRITICAL: Check if target looks like a function entry (has prologue) // If branching to a prologue, it's definitely a tail call to another function if (!is_tail_call && is_prologue_pattern(target)) { REXCODEGEN_TRACE("discover_blocks: target 0x{:08X} has prologue pattern (TAIL CALL)", target); is_tail_call = true; } if (is_tail_call) { REXCODEGEN_TRACE("discover_blocks: b 0x{:08X} -> 0x{:08X} is TAIL CALL", addr, target); result.tail_calls.push_back(target); } else if (!scannedAddrs.count(target)) { REXCODEGEN_TRACE( "discover_blocks: b 0x{:08X} -> 0x{:08X} treated as INTERNAL (entry=0x{:08X})", addr, target, entry_point); // Carry projection forward if branch is continuous bool is_continuous = (target == block.end); int64_t carry_projection = -1; if (is_continuous && block.projectedSize != -1) { carry_projection = block.projectedSize - static_cast(block.end - block.base); if (carry_projection <= 0) carry_projection = -1; } // IMPORTANT: Save and pop current block FIRST, before push_back // push_back can reallocate the vector, invalidating 'block' reference block.has_terminator = true; result.blocks.push_back(block); block_stack.pop_back(); // Now safe to push new block DiscoveredBlock target_block; target_block.base = target; target_block.end = target; target_block.projectedSize = carry_projection; block_stack.push_back(target_block); continue; } } block.has_terminator = true; result.blocks.push_back(block); block_stack.pop_back(); continue; } // Check for function call (bl) - doesn't end block if (instr.is_call() && instr.branch_target.has_value()) { result.external_calls.push_back(instr.branch_target.value()); // Continue to next instruction (block.end already updated) continue; } // Check for conditional return (bclr/bclrl with conditional BO) // These return to LR if condition is met, otherwise fall through // Examples: blelr, bgtlr, bnelr, beqlr, etc. if ((instr.opcode == Opcode::bclr || instr.opcode == Opcode::bclrl) && !instr.is_return()) { // is_return() only matches unconditional blr guest_addr_t fall_through = addr + 4; block.successors.push_back(fall_through); block.has_terminator = true; result.blocks.push_back(block); block_stack.pop_back(); // Pop current block BEFORE pushing fall-through // Push fall-through block onto stack if (!scannedAddrs.count(fall_through)) { DiscoveredBlock ft_block; ft_block.base = fall_through; ft_block.end = fall_through; ft_block.projectedSize = -1; block_stack.push_back(ft_block); } continue; } // Check for conditional branch (bc, bca, etc.) if (instr.is_branch() && instr.branch_target.has_value()) { guest_addr_t target = instr.branch_target.value(); guest_addr_t fall_through = addr + 4; // Block ends at conditional branch block.successors.push_back(fall_through); block.successors.push_back(target); result.blocks.push_back(block); block_stack.pop_back(); // Push true-case first, then false-case // False-case (fall-through) gets projectedSize = distance to true-case // This prevents fall-through from growing past the branch target // Check if target is internal to function (at or after entry point) bool target_is_internal = (target >= entry_point); // Push true-case block (branch target) - no projection if (target_is_internal && !scannedAddrs.count(target)) { DiscoveredBlock true_block; true_block.base = target; true_block.end = target; true_block.projectedSize = -1; // No limit on true-case block_stack.push_back(true_block); } // Push false-case block (fall-through) WITH projection if (!scannedAddrs.count(fall_through)) { DiscoveredBlock false_block; false_block.base = fall_through; false_block.end = fall_through; // Project size: distance from fall-through to branch target // This prevents fall-through from consuming code past the branch target if (target_is_internal && target > fall_through) { false_block.projectedSize = static_cast(target - fall_through); REXCODEGEN_TRACE( "Conditional branch at 0x{:08X}: fall-through 0x{:08X} projected to 0x{:X} bytes", addr, fall_through, false_block.projectedSize); } else { false_block.projectedSize = -1; } block_stack.push_back(false_block); } continue; } // Regular instruction - continue to next (block.end already updated) } if (result.blocks.empty()) { REXCODEGEN_WARN("discover_blocks: no blocks found for entry 0x{:08X}", entry_point); } // Sort blocks by address for deterministic output and easier diffing std::sort(result.blocks.begin(), result.blocks.end(), [](const DiscoveredBlock& a, const DiscoveredBlock& b) { return a.base < b.base; }); return result; } //============================================================================= // Code Region Boundary Checking //============================================================================= const CodeRegion* FunctionScanner::findRegionContaining(uint32_t address) const { if (!code_regions_) return nullptr; for (const auto& region : *code_regions_) { if (region.contains(address)) { return ®ion; } } return nullptr; } bool FunctionScanner::isInternalBranch(uint32_t currentAddr, uint32_t targetAddr, uint32_t functionEntry) const { // Check if target is a configured chunk of the current function // Chunks can cross region boundaries by design if (isWithinChunk(targetAddr, functionEntry)) { return true; } // Find regions for both addresses const auto* currentRegion = findRegionContaining(currentAddr); const auto* targetRegion = findRegionContaining(targetAddr); // If target is in a different region (or no region), it's a tail call if (currentRegion != targetRegion) { REXCODEGEN_TRACE("isInternalBranch: 0x{:08X} -> 0x{:08X} crosses region boundary (TAIL CALL)", currentAddr, targetAddr); return false; } return true; } //============================================================================= // Block Discovery //============================================================================= namespace { //============================================================================= // Helper: Check if instruction is prologue pattern //============================================================================= [[maybe_unused]] bool isProloguePattern(const DecodedInsn& insn) { using namespace rex::codegen::ppc; switch (insn.opcode) { case Opcode::mflr: case Opcode::mfspr: // mflr is really mfspr with SPR=8 return true; case Opcode::stwu: // stwu r1, -X(r1) - stack frame setup return insn.D.RA == 1 && insn.D.RT == 1 && static_cast(insn.D.d) < 0; default: return false; } } //============================================================================= // Helper: Check if block should stop at this instruction //============================================================================= bool isBlockTerminator(const DecodedInsn& insn, uint32_t addr, const CodeRegion& region, const std::unordered_set& knownFunctions) { using namespace rex::codegen::ppc; // NULL padding ends block (but NOT unknown instructions - those get emitted as comments) uint32_t raw = static_cast(insn.code); if (raw == 0x00000000 || raw == 0xFFFFFFFF) { return true; } // Note: kUnknown opcodes (like 64-bit rotate instructions) are NOT terminators. // They should be included in the block and emitted as comments during codegen. // Check for terminators if (isReturn(insn)) return true; // bcctr (indirect branch via CTR) if (insn.opcode == Opcode::bcctr || insn.opcode == Opcode::bcctrl) { // bcctrl is call, bcctr is terminator return insn.opcode == Opcode::bcctr; } // Unconditional branch if (isBranch(insn) && !isConditional(insn) && !isCall(insn)) { auto target = getBranchTarget(insn); if (target) { // Branch outside region is terminator if (!region.contains(*target)) return true; // Branch to known function is tail call (terminator) if (knownFunctions.contains(*target)) return true; } return true; // Unconditional branch always terminates block } return false; } //============================================================================= // Helper: Detect bounds check for jump table //============================================================================= BoundsInfo scanForBounds(DecodedBinary& decoded, uint32_t bctrAddr, const CodeRegion& region, uint8_t expectedReg, uint32_t funcStart) { BoundsInfo result; const int backwardScanLimit = static_cast(REXCVAR_GET(backward_scan_limit)); // Use funcStart as lower bound to avoid scanning into other functions uint32_t scanLowerBound = std::max(region.start, funcStart); REXCODEGEN_TRACE( "scanForBounds: bctr=0x{:08X} region=[0x{:08X}-0x{:08X}] funcStart=0x{:08X} expectedReg=r{}", bctrAddr, region.start, region.end, funcStart, expectedReg); uint32_t scanAddr = bctrAddr; for (int i = 0; i < backwardScanLimit && scanAddr >= scanLowerBound + 4; i++) { scanAddr -= 4; auto* insn = decoded.get(scanAddr); if (!insn) break; // EA commonly emits switch bounds as: // cmplwi idx,max; ble table_path; b default_path; table_path: ... bctr // Keep scanning past that unconditional default branch so the compare is // visible to the jump-table reader. if (isTerminator(*insn) && !isConditional(*insn) && insn->opcode != rex::codegen::ppc::Opcode::b && insn->opcode != rex::codegen::ppc::Opcode::ba) break; using namespace rex::codegen::ppc; // Look for cmpli/cmpi followed by conditional branch if (insn->opcode == Opcode::cmpli) { // cmpli crX, L, rA, UIMM REXCODEGEN_TRACE("scanForBounds: found cmpli at 0x{:08X} RA=r{} UIMM={} (expecting r{})", scanAddr, static_cast(insn->D.RA), static_cast(insn->D.d), expectedReg); if (insn->D.RA == expectedReg) { result.maxEntries = static_cast(insn->D.d) + 1; result.indexReg = expectedReg; result.found = true; REXCODEGEN_TRACE("scanForBounds: MATCHED! maxEntries={}", result.maxEntries); return result; } } if (insn->opcode == Opcode::cmpi) { // cmpi crX, L, rA, SIMM REXCODEGEN_TRACE("scanForBounds: found cmpi at 0x{:08X} RA=r{} SIMM={} (expecting r{})", scanAddr, static_cast(insn->D.RA), static_cast(insn->D.d), expectedReg); if (insn->D.RA == expectedReg) { result.maxEntries = static_cast(insn->D.d) + 1; result.indexReg = expectedReg; result.found = true; REXCODEGEN_TRACE("scanForBounds: MATCHED! maxEntries={}", result.maxEntries); return result; } } // Look for clrlwi (rlwinm rA, rS, 0, MB, 31) which masks bits // MB must be > 0 to actually mask something; MB=0 is a no-op if (insn->opcode == Opcode::rlwinm) { if (insn->M.RA == expectedReg && insn->M.SH == 0 && insn->M.ME == 31 && insn->M.MB > 0) { // Masked to (32 - MB) bits, max value is 2^(32-MB) - 1 uint32_t bits = 32 - insn->M.MB; result.maxEntries = 1u << bits; result.indexReg = expectedReg; result.found = true; REXCODEGEN_TRACE("scanForBounds: found clrlwi at 0x{:08X} MB={} maxEntries={}", scanAddr, static_cast(insn->M.MB), result.maxEntries); return result; } } } REXCODEGEN_TRACE("scanForBounds: no bounds found for bctr=0x{:08X}", bctrAddr); return result; } } // anonymous namespace //============================================================================= // Jump Table Detection //============================================================================= std::optional detectJumpTable(DecodedBinary& decoded, uint32_t bctrAddr, const CodeRegion& containingRegion, uint32_t funcStart, uint32_t funcEnd) { using namespace rex::codegen::ppc; const int kMaxBackwardScan = static_cast(REXCVAR_GET(backward_scan_limit)); const uint32_t kMaxTableEntries = REXCVAR_GET(max_jump_table_entries); // State for backward scan uint8_t ctrSourceReg = 0xFF; uint32_t tableAddr = 0; uint32_t baseAddr = 0; // Pending address parts (order of lis/addi varies in backward scan) // Note: addi uses sign-extended addition, ori uses OR uint32_t pendingTableLo = 0, pendingTableHi = 0; uint32_t pendingBaseLo = 0, pendingBaseHi = 0; bool hasPendingTableLo = false, hasPendingTableHi = false; bool hasPendingBaseLo = false, hasPendingBaseHi = false; bool pendingTableLoIsAddi = false, pendingBaseLoIsAddi = false; // Helper to combine lis high bits with addi/ori low bits auto combineHiLo = [](uint32_t hi, uint32_t lo, bool isAddi) -> uint32_t { if (isAddi) { // addi: sign-extend lo and add return hi + static_cast(static_cast(static_cast(lo))); } else { // ori: zero-extend and OR return hi | lo; } }; JumpTableType tableType = JumpTableType::kAbsolute; uint8_t indexReg = 0xFF; // Current reg being traced (0xFF = stop tracing) uint8_t finalIndexReg = 0xFF; // Last valid indexReg for scanForBounds/output int shiftAmount = 0; // Backward scan from bctr uint32_t scanAddr = bctrAddr; bool foundMtctr = false; bool foundLoad = false; for (int i = 0; i < kMaxBackwardScan && scanAddr >= containingRegion.start + 4; i++) { scanAddr -= 4; auto* insn = decoded.get(scanAddr); if (!insn) break; // Stop at unconditional terminators (but NOT conditional branches - they're often bounds // checks) if (isTerminator(*insn) && !isConditional(*insn)) break; // Find mtctr rX if (!foundMtctr) { if (insn->opcode == Opcode::mtctr || insn->opcode == Opcode::mtspr) { // mtctr is mtspr 9, rS ctrSourceReg = insn->XFX.RT; foundMtctr = true; continue; } } // After mtctr, look for load into ctrSourceReg if (foundMtctr && !foundLoad) { // lwzx rD, rA, rB - indexed word load (ABSOLUTE table) if (insn->opcode == Opcode::lwzx && insn->X.RT == ctrSourceReg) { // Table address is in rA, index scaled in rB tableType = JumpTableType::kAbsolute; indexReg = insn->X.RB; finalIndexReg = indexReg; foundLoad = true; REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} found lwzx at 0x{:08X}", bctrAddr, scanAddr); continue; } // lbzx rD, rA, rB - indexed byte load (BYTE/COMPUTED table) if (insn->opcode == Opcode::lbzx && insn->X.RT == ctrSourceReg) { // Don't overwrite kComputed (set by rlwinm for shifted byte tables) if (tableType != JumpTableType::kComputed) { tableType = JumpTableType::kByteOffset; } indexReg = insn->X.RB; finalIndexReg = indexReg; foundLoad = true; REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} found lbzx at 0x{:08X}", bctrAddr, scanAddr); continue; } // lhzx rD, rA, rB - indexed halfword load (SHORTOFFSET table) if (insn->opcode == Opcode::lhzx && insn->X.RT == ctrSourceReg) { // Don't overwrite kComputed if (tableType != JumpTableType::kComputed) { tableType = JumpTableType::kShortOffset; } indexReg = insn->X.RB; finalIndexReg = indexReg; foundLoad = true; REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} found lhzx at 0x{:08X}", bctrAddr, scanAddr); continue; } // add rD, rA, rB - combining base with offset // Pattern: rD = base + offset, where one operand is base, other is from table if (insn->opcode == Opcode::add && insn->XO.RT == ctrSourceReg) { // If RA == rD (e.g., r12 = r12 + r0), then RB has the table offset // If RA != rD (e.g., r12 = r11 + r0), follow RA for the chain if (insn->XO.RA == ctrSourceReg) { // Pattern: r12 = r12 + r0 --> r0 came from table load ctrSourceReg = insn->XO.RB; } else { // Follow RA ctrSourceReg = insn->XO.RA; } REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} found add at 0x{:08X}, now tracking r{}", bctrAddr, scanAddr, ctrSourceReg); continue; } // rlwinm - shift for computed offset (slwi is rlwinm simplified) if (insn->opcode == Opcode::rlwinm && insn->M.RA == ctrSourceReg) { shiftAmount = insn->M.SH; if (shiftAmount > 0) { tableType = JumpTableType::kComputed; } ctrSourceReg = insn->M.RS; REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} found rlwinm at 0x{:08X}", bctrAddr, scanAddr); continue; } // Log unhandled instructions in the chain (potential issue) REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} unhandled insn at 0x{:08X} opcode={} while looking for " "load into r{}", bctrAddr, scanAddr, static_cast(insn->opcode), ctrSourceReg); } // After load: trace back indexReg through LEFT SHIFT (slwi) instructions only // slwi rA, rS, n is rlwinm rA, rS, n, 0, 31-n (MB=0, ME=31-SH) // This scales the index for table lookup (e.g., slwi r0, r31, 1 for halfword table) // DON'T trace back through extrwi/other rlwinm variants - those transform the value // NOTE: SH must be > 0 for a real shift; SH=0 is just a move/no-op (clrlwi r,r,0) // IMPORTANT: Stop tracing if another instruction writes to indexReg (breaks the chain) if (foundLoad && indexReg != 0xFF) { // Check if this instruction writes to indexReg bool writesToIndexReg = false; uint8_t destReg = 0xFF; // Check common instruction forms that write to a register if (insn->opcode == Opcode::rlwinm) { destReg = insn->M.RA; } else if (insn->opcode == Opcode::srawi || insn->opcode == Opcode::sraw || insn->opcode == Opcode::srw || insn->opcode == Opcode::slw) { // X-form shift instructions: destination is RA (bits 11-15) destReg = insn->X.RA; } else if (insn->opcode == Opcode::lbz || insn->opcode == Opcode::lhz || insn->opcode == Opcode::lwz || insn->opcode == Opcode::li || insn->opcode == Opcode::lis || insn->opcode == Opcode::addi) { destReg = insn->D.RT; } else if (insn->opcode == Opcode::lbzx || insn->opcode == Opcode::lhzx || insn->opcode == Opcode::lwzx) { // X-form load instructions: destination is RT (bits 6-10) destReg = insn->X.RT; } else if (insn->opcode == Opcode::or_ || insn->opcode == Opcode::and_ || insn->opcode == Opcode::xor_ || insn->opcode == Opcode::mr) { // X-form logical instructions: destination is RA (bits 11-15), NOT RT // (RT is RS/source for these instructions) destReg = insn->X.RA; } else if (insn->opcode == Opcode::add || insn->opcode == Opcode::subf) { // XO-form instructions (add, subf) destReg = insn->XO.RT; } if (destReg == indexReg) { writesToIndexReg = true; // Check for slwi pattern: if it matches, trace back; otherwise stop tracing if (insn->opcode == Opcode::rlwinm) { uint8_t sh = insn->M.SH; uint8_t mb = insn->M.MB; uint8_t me = insn->M.ME; // Check for slwi pattern: SH>0, MB=0, ME=31-SH if (sh > 0 && mb == 0 && me == (31 - sh)) { indexReg = insn->M.RS; finalIndexReg = indexReg; REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} found slwi at 0x{:08X} indexReg now r{}", bctrAddr, scanAddr, indexReg); } else { // Non-slwi rlwinm writes to indexReg, stop tracing REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} indexReg r{} overwritten by non-slwi rlwinm at " "0x{:08X}, stop tracing", bctrAddr, indexReg, scanAddr); indexReg = 0xFF; // Mark as invalid to stop further tracing } } else { // Another instruction writes to indexReg, stop tracing REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} indexReg r{} overwritten at 0x{:08X}, stop tracing", bctrAddr, indexReg, scanAddr); indexReg = 0xFF; // Mark as invalid to stop further tracing } } } // Find lis/addi pairs for table and base addresses // When scanning backward for byte offset tables: // - BEFORE foundLoad (between mtctr and lbzx): baseAddr // - AFTER foundLoad (before lbzx in forward order): tableAddr // For absolute tables (lwzx), there's only tableAddr (after foundLoad) if (foundMtctr) { // lis rD, HI if (insn->opcode == Opcode::lis) { uint32_t hi = static_cast(static_cast(insn->D.d)) << 16; REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} found lis at 0x{:08X} hi=0x{:08X} foundLoad={}", bctrAddr, scanAddr, hi, foundLoad); if (foundLoad) { // After load: this is tableAddr (only capture first complete address) if (tableAddr == 0) { tableAddr = hasPendingTableLo ? combineHiLo(hi, pendingTableLo, pendingTableLoIsAddi) : hi; hasPendingTableLo = false; REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} set tableAddr=0x{:08X}", bctrAddr, tableAddr); } // Once tableAddr is set, ignore further lis instructions } else { // Before load (between mtctr and load): this is baseAddr if (baseAddr == 0) { baseAddr = hasPendingBaseLo ? combineHiLo(hi, pendingBaseLo, pendingBaseLoIsAddi) : hi; hasPendingBaseLo = false; REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} set baseAddr=0x{:08X}", bctrAddr, baseAddr); } // Once baseAddr is set, ignore further lis instructions } } // addi rD, rA, LO (ori also possible) if (insn->opcode == Opcode::addi || insn->opcode == Opcode::ori) { uint32_t lo = static_cast(insn->D.d); bool isAddi = (insn->opcode == Opcode::addi); REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} found {} at 0x{:08X} lo=0x{:04X} foundLoad={}", bctrAddr, isAddi ? "addi" : "ori", scanAddr, lo, foundLoad); if (foundLoad) { // After load: this is tableAddr (only capture first complete address) if (tableAddr == 0) { if (hasPendingTableHi) { tableAddr = combineHiLo(pendingTableHi, lo, isAddi); hasPendingTableHi = false; REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} set tableAddr=0x{:08X} from pending", bctrAddr, tableAddr); } else if (!hasPendingTableLo) { pendingTableLo = lo; pendingTableLoIsAddi = isAddi; hasPendingTableLo = true; REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} pending tableLo=0x{:04X}", bctrAddr, lo); } } else if ((tableAddr & 0xFFFF) == 0) { // tableAddr has only high bits, add low bits tableAddr = combineHiLo(tableAddr, lo, isAddi); REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} combined tableAddr=0x{:08X}", bctrAddr, tableAddr); } // Once tableAddr is fully set, ignore further addi instructions } else { // Before load: this is baseAddr if (baseAddr == 0) { if (hasPendingBaseHi) { baseAddr = combineHiLo(pendingBaseHi, lo, isAddi); hasPendingBaseHi = false; REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} set baseAddr=0x{:08X} from pending", bctrAddr, baseAddr); } else if (!hasPendingBaseLo) { pendingBaseLo = lo; pendingBaseLoIsAddi = isAddi; hasPendingBaseLo = true; REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} pending baseLo=0x{:04X}", bctrAddr, lo); } } else if ((baseAddr & 0xFFFF) == 0) { baseAddr = combineHiLo(baseAddr, lo, isAddi); REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} combined baseAddr=0x{:08X}", bctrAddr, baseAddr); } // Once baseAddr is fully set, ignore further addi instructions } } } } REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} scan complete: foundMtctr={} foundLoad={} tableAddr=0x{:08X} " "baseAddr=0x{:08X}", bctrAddr, foundMtctr, foundLoad, tableAddr, baseAddr); if (!foundMtctr || !foundLoad || tableAddr == 0) { REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} FAILED foundMtctr={} foundLoad={} tableAddr=0x{:08X}", bctrAddr, foundMtctr, foundLoad, tableAddr); return std::nullopt; } // For offset-based tables, we need a base address if (tableType != JumpTableType::kAbsolute && baseAddr == 0) { baseAddr = containingRegion.start; // Fallback to region start } // Find bounds auto bounds = scanForBounds(decoded, bctrAddr, containingRegion, finalIndexReg, funcStart); // If bounds not found (e.g., state machine pattern with forward bounds check), // use max entries and let the validation loop determine actual table size uint32_t entryCount = bounds.found ? bounds.maxEntries : kMaxTableEntries; // Read table entries JumpTable jt; jt.bctrAddress = bctrAddr; jt.tableAddress = tableAddr; jt.indexRegister = finalIndexReg; REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} reading {} entries from table=0x{:08X} base=0x{:08X} type={}", bctrAddr, entryCount, tableAddr, baseAddr, static_cast(tableType)); for (uint32_t i = 0; i < entryCount; i++) { uint32_t target = 0; switch (tableType) { case JumpTableType::kAbsolute: { auto val = decoded.read(tableAddr + i * 4); if (!val) { REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} entry[{}] read failed at 0x{:08X}", bctrAddr, i, tableAddr + i * 4); break; } target = *val; break; } case JumpTableType::kByteOffset: { auto val = decoded.read(tableAddr + i); if (!val) { REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} entry[{}] read failed at 0x{:08X}", bctrAddr, i, tableAddr + i); break; } target = baseAddr + *val; REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} entry[{}] offset=0x{:02X} target=0x{:08X}", bctrAddr, i, *val, target); break; } case JumpTableType::kComputed: { auto val = decoded.read(tableAddr + i); if (!val) break; target = baseAddr + (static_cast(*val) << shiftAmount); break; } case JumpTableType::kShortOffset: { auto val = decoded.read(tableAddr + i * 2); if (!val) break; target = baseAddr + *val; break; } } // PPC instructions must be 4-byte aligned if (target & 3) { REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} entry[{}] target=0x{:08X} not 4-byte aligned", bctrAddr, i, target); if (jt.targets.empty()) return std::nullopt; break; } // Validate target is within code region - jump table targets help DEFINE function extent // Don't constrain by funcEnd since that's just PDATA which may not include out-of-line code if (target == 0 || !containingRegion.contains(target)) { // TODO(tomc): Figure out what this voodoo does on real hardware. Its a jump target that // points to a null value..? if (target != 0) { auto outsideInsn = decoded.read(target); if (outsideInsn && (*outsideInsn == 0x00000000 || *outsideInsn == 0xFFFFFFFF)) { REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} entry[{}] target=0x{:08X} null jump sentinel", bctrAddr, i, target); jt.targets.push_back(0); // sentinel continue; } } // End of valid entries REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} entry[{}] target=0x{:08X} invalid (region " "0x{:08X}-0x{:08X})", bctrAddr, i, target, containingRegion.start, containingRegion.end); if (jt.targets.empty()) return std::nullopt; break; } // Target must be >= function start (can't jump backward past entry point) if (target < funcStart) { REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} entry[{}] target=0x{:08X} < funcStart=0x{:08X}", bctrAddr, i, target, funcStart); if (jt.targets.empty()) return std::nullopt; break; } // Validate target points to valid code, not null padding // TODO(tomc): look into this more. what is the expected behavior when the processor executes // a null instruction. auto targetInsn = decoded.read(target); if (targetInsn && (*targetInsn == 0x00000000 || *targetInsn == 0xFFFFFFFF)) { REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} entry[{}] target=0x{:08X} points to null/padding " "(0x{:08X})", bctrAddr, i, target, *targetInsn); jt.targets.push_back(0); // sentinel, handled in codegen as __builtin_trap() continue; } jt.targets.push_back(target); } if (jt.targets.empty()) { REXCODEGEN_TRACE( "detectJumpTable: bctr=0x{:08X} table=0x{:08X} NO VALID TARGETS (funcStart=0x{:08X} " "funcEnd=0x{:08X})", bctrAddr, tableAddr, funcStart, funcEnd); return std::nullopt; } REXCODEGEN_TRACE("detectJumpTable: bctr=0x{:08X} table=0x{:08X} entries={} funcEnd=0x{:08X}", bctrAddr, tableAddr, jt.targets.size(), funcEnd); return jt; } //============================================================================= // Block Discovery //============================================================================= BlockDiscoveryResult discoverBlocks(DecodedBinary& decoded, uint32_t entryPoint, const CodeRegion& containingRegion, const std::unordered_set& knownFunctions, uint32_t pdataSize) { BlockDiscoveryResult result; std::unordered_set visited; std::unordered_set blockStarts; std::queue worklist; // Function extent - use pdataSize when available uint32_t funcEnd = (pdataSize > 0) ? (entryPoint + pdataSize) : containingRegion.end; REXCODEGEN_TRACE( "discoverBlocks: entry=0x{:08X} pdataSize={} funcEnd=0x{:08X} region=[0x{:08X}-0x{:08X}]", entryPoint, pdataSize, funcEnd, containingRegion.start, containingRegion.end); // Helper to check if address is within function bounds auto isWithinFunction = [&](uint32_t addr) -> bool { return addr >= entryPoint && addr < funcEnd; }; // Start with entry point worklist.push(entryPoint); blockStarts.insert(entryPoint); while (!worklist.empty()) { uint32_t blockStart = worklist.front(); worklist.pop(); if (visited.contains(blockStart)) continue; if (!isWithinFunction(blockStart)) continue; // Linear scan until terminator uint32_t addr = blockStart; Block block; block.base = blockStart; block.size = 0; while (isWithinFunction(addr)) { auto* insn = decoded.get(addr); if (!insn) { REXCODEGEN_TRACE("discoverBlocks: 0x{:08X} no instruction at addr, breaking", entryPoint); break; } // Mark as visited visited.insert(addr); // Collect instruction pointer result.instructions.push_back(insn); // Handle branches if (isBranch(*insn)) { auto target = getBranchTarget(*insn); // Helper: check if target is internal to this function // Uses funcEnd (from pdataSize or region) defined at top of function auto isInternalTarget = [&](uint32_t t) -> bool { // Must be within function bounds if (t < entryPoint || t >= funcEnd) { return false; } // Must not be a known function entry (except our own entry point) if (t != entryPoint && knownFunctions.contains(t)) { return false; } return true; }; if (isCall(*insn)) { // bl - function call if (target) { // All bl instructions need to be recorded as unresolved branches // so they can be resolved to CallEdges during merge phase result.unresolvedBranches.push_back({addr, *target, true, false}); // Track as external call for function discovery if (!isInternalTarget(*target)) { result.externalCalls.push_back(*target); } } // Calls don't terminate block, fall through } else if (isReturn(*insn)) { // blr - end of function path block.size = addr - blockStart + 4; break; } else if (insn->opcode == rex::codegen::ppc::Opcode::bcctr) { // bctr - try to detect jump table REXCODEGEN_TRACE("discoverBlocks: bctr at 0x{:08X} in func 0x{:08X}, funcEnd=0x{:08X}", addr, entryPoint, funcEnd); auto jt = detectJumpTable(decoded, addr, containingRegion, entryPoint, funcEnd); if (jt) { REXCODEGEN_TRACE("discoverBlocks: detected jump table at bctr 0x{:08X} with {} targets", addr, jt->targets.size()); result.jumpTables.push_back(*jt); // Jump table targets are definitionally part of this function // Extend funcEnd if any target exceeds it (within region bounds) // This handles out-of-line switch case code for (uint32_t t : jt->targets) { if (t == 0) continue; // sentinel from null-padding detection if (t >= funcEnd && t < containingRegion.end) { funcEnd = t + 4; // Extend to include this target } result.labels.insert(t); if (!visited.contains(t) && !blockStarts.contains(t)) { blockStarts.insert(t); worklist.push(t); } } } block.size = addr - blockStart + 4; break; } else if (isConditional(*insn)) { // Conditional branch - follow both paths if (target && isInternalTarget(*target)) { result.labels.insert(*target); if (!visited.contains(*target) && !blockStarts.contains(*target)) { blockStarts.insert(*target); worklist.push(*target); } } else if (target) { // External conditional branch (or conditional tail call to known function) result.unresolvedBranches.push_back({addr, *target, false, true}); } // CRITICAL: Fall-through also needs a label uint32_t fallthrough = addr + 4; if (isInternalTarget(fallthrough)) { result.labels.insert(fallthrough); if (!visited.contains(fallthrough) && !blockStarts.contains(fallthrough)) { blockStarts.insert(fallthrough); worklist.push(fallthrough); } } } else { // Unconditional branch if (target) { if (isInternalTarget(*target)) { // Internal unconditional branch (includes backward branches) result.labels.insert(*target); if (!visited.contains(*target) && !blockStarts.contains(*target)) { blockStarts.insert(*target); worklist.push(*target); } } else { // Tail call to external result.tailCalls.push_back(*target); result.unresolvedBranches.push_back({addr, *target, false, false}); } } block.size = addr - blockStart + 4; break; } } // Check for block terminator (null, prologue of next function, etc.) if (isBlockTerminator(*insn, addr, containingRegion, knownFunctions)) { REXCODEGEN_TRACE("discoverBlocks: 0x{:08X} block terminator at 0x{:08X}", entryPoint, addr); block.size = addr - blockStart + 4; break; } addr += 4; } // Log why loop exited if not due to terminator if (block.size == 0 && !isWithinFunction(addr)) { REXCODEGEN_TRACE("discoverBlocks: 0x{:08X} addr 0x{:08X} outside function (funcEnd=0x{:08X})", entryPoint, addr, funcEnd); } // Finalize block size if not set if (block.size == 0) { block.size = addr - blockStart; } if (block.size > 0) { result.blocks.push_back(block); } } // Sort blocks by address std::sort(result.blocks.begin(), result.blocks.end(), [](const Block& a, const Block& b) { return a.base < b.base; }); // Remove duplicate instructions (in case of overlapping scans) std::sort(result.instructions.begin(), result.instructions.end(), [](auto* a, auto* b) { return a->address < b->address; }); result.instructions.erase(std::unique(result.instructions.begin(), result.instructions.end()), result.instructions.end()); REXCODEGEN_TRACE("discoverBlocks: entry=0x{:08X} blocks={} instructions={} labels={}", entryPoint, result.blocks.size(), result.instructions.size(), result.labels.size()); return result; } } // namespace rex::codegen