#include "psprecomp/program_analysis.hpp" #include "psprecomp/decoder.hpp" #include #include #include #include #include #include namespace psprecomp { namespace { std::vector executable_ranges_for(const Elf32Image &elf, std::uint32_t load_base) { std::vector ranges; for (std::size_t i = 0; i < elf.segments().size(); ++i) { const auto &segment = elf.segments()[i]; if (segment.type != 1u || (segment.flags & 1u) == 0u || segment.file_size == 0u) continue; const std::uint32_t start = elf.segment_runtime_address(i, load_base); const std::uint64_t end64 = static_cast(start) + segment.file_size; if (end64 > std::numeric_limits::max()) continue; ranges.push_back({start, static_cast(end64)}); } std::sort(ranges.begin(), ranges.end(), [](const auto &a, const auto &b) { return a.start < b.start; }); return ranges; } std::uint32_t direct_jump_target(std::uint32_t pc, const DecodedInstruction &decoded) { return ((pc + 4u) & 0xF0000000u) | (decoded.target << 2u); } std::uint32_t branch_target(std::uint32_t pc, const DecodedInstruction &decoded) { const auto displacement = static_cast(decoded.immediate) * 4; return pc + 4u + static_cast(displacement); } bool is_conditional_branch(OpcodeKind kind) { switch (kind) { case OpcodeKind::Beq: case OpcodeKind::Bne: case OpcodeKind::Beql: case OpcodeKind::Bnel: case OpcodeKind::Blez: case OpcodeKind::Bgtz: case OpcodeKind::Blezl: case OpcodeKind::Bgtzl: case OpcodeKind::Bltz: case OpcodeKind::Bgez: case OpcodeKind::Bltzl: case OpcodeKind::Bgezl: case OpcodeKind::Bltzal: case OpcodeKind::Bgezal: case OpcodeKind::Bltzall: case OpcodeKind::Bgezall: case OpcodeKind::Bc1f: case OpcodeKind::Bc1t: case OpcodeKind::Bc1fl: case OpcodeKind::Bc1tl: case OpcodeKind::Bvf: case OpcodeKind::Bvt: case OpcodeKind::Bvfl: case OpcodeKind::Bvtl: return true; default: return false; } } void add_seed(std::map &seeds, const std::vector &ranges, std::uint32_t address, const char *source) { if (is_executable_address(ranges, address)) seeds.try_emplace(address, source); } std::set collect_global_block_starts(const GuestMemory &memory, const std::vector &ranges) { std::set starts; for (const auto &range : ranges) { starts.insert(range.start); for (std::uint32_t pc = range.start; pc + 4u <= range.end; pc += 4u) { const auto decoded = decode_allegrex(memory.load32(pc)); if (is_conditional_branch(decoded.kind)) { const auto target = branch_target(pc, decoded); if (is_executable_address(ranges, target)) starts.insert(target); if (is_executable_address(ranges, pc + 8u)) starts.insert(pc + 8u); } else if (decoded.kind == OpcodeKind::J) { const auto target = direct_jump_target(pc, decoded); if (is_executable_address(ranges, target)) starts.insert(target); } else if (decoded.kind == OpcodeKind::Jal || decoded.kind == OpcodeKind::Jalr) { if (is_executable_address(ranges, pc + 8u)) starts.insert(pc + 8u); } } } return starts; } using ConstantState = std::array, 32>; void clear_all_constants(ConstantState &constants) { for (auto &value : constants) value.reset(); constants[0] = 0u; } void set_constant_and_seed(ConstantState &constants, std::uint32_t reg_index, std::optional value, std::map &seeds, const std::vector &ranges) { if (reg_index == 0u) return; constants[reg_index] = value; if (value && reg_index != 31u) add_seed(seeds, ranges, *value, "materialized_code_pointer"); } void propagate_constant(const DecodedInstruction &decoded, std::uint32_t pc, ConstantState &constants, std::map &seeds, const std::vector &ranges) { const auto lhs = constants[decoded.rs]; const auto rhs = constants[decoded.rt]; const auto simm = static_cast(decoded.immediate); const auto uimm = static_cast(decoded.immediate); auto binary = [&](auto operation) -> std::optional { if (!lhs || !rhs) return std::nullopt; return static_cast(operation(*lhs, *rhs)); }; switch (decoded.kind) { case OpcodeKind::Lui: set_constant_and_seed(constants, decoded.rt, uimm << 16u, seeds, ranges); break; case OpcodeKind::Addiu: set_constant_and_seed(constants, decoded.rt, lhs ? std::optional(*lhs + static_cast(simm)) : std::nullopt, seeds, ranges); break; case OpcodeKind::Andi: set_constant_and_seed(constants, decoded.rt, lhs ? std::optional(*lhs & uimm) : std::nullopt, seeds, ranges); break; case OpcodeKind::Ori: set_constant_and_seed(constants, decoded.rt, lhs ? std::optional(*lhs | uimm) : std::nullopt, seeds, ranges); break; case OpcodeKind::Xori: set_constant_and_seed(constants, decoded.rt, lhs ? std::optional(*lhs ^ uimm) : std::nullopt, seeds, ranges); break; case OpcodeKind::Slti: case OpcodeKind::Sltiu: set_constant_and_seed(constants, decoded.rt, std::nullopt, seeds, ranges); break; case OpcodeKind::Addu: set_constant_and_seed(constants, decoded.rd, binary([](auto a, auto b) { return a + b; }), seeds, ranges); break; case OpcodeKind::Subu: set_constant_and_seed(constants, decoded.rd, binary([](auto a, auto b) { return a - b; }), seeds, ranges); break; case OpcodeKind::And: set_constant_and_seed(constants, decoded.rd, binary([](auto a, auto b) { return a & b; }), seeds, ranges); break; case OpcodeKind::Or: set_constant_and_seed(constants, decoded.rd, binary([](auto a, auto b) { return a | b; }), seeds, ranges); break; case OpcodeKind::Xor: set_constant_and_seed(constants, decoded.rd, binary([](auto a, auto b) { return a ^ b; }), seeds, ranges); break; case OpcodeKind::Nor: set_constant_and_seed(constants, decoded.rd, binary([](auto a, auto b) { return ~(a | b); }), seeds, ranges); break; case OpcodeKind::Sll: set_constant_and_seed(constants, decoded.rd, rhs ? std::optional(*rhs << decoded.sa) : std::nullopt, seeds, ranges); break; case OpcodeKind::Srl: set_constant_and_seed(constants, decoded.rd, rhs ? std::optional(*rhs >> decoded.sa) : std::nullopt, seeds, ranges); break; case OpcodeKind::Sra: set_constant_and_seed(constants, decoded.rd, rhs ? std::optional(static_cast(static_cast(*rhs) >> decoded.sa)) : std::nullopt, seeds, ranges); break; case OpcodeKind::Ext: { const std::uint32_t size = decoded.rd + 1u; const std::uint32_t mask = size == 32u ? 0xFFFFFFFFu : ((1u << size) - 1u); set_constant_and_seed(constants, decoded.rt, lhs ? std::optional((*lhs >> decoded.sa) & mask) : std::nullopt, seeds, ranges); break; } case OpcodeKind::Ins: { const std::uint32_t size = decoded.rd >= decoded.sa ? decoded.rd - decoded.sa + 1u : 0u; const std::uint32_t source_mask = size == 32u ? 0xFFFFFFFFu : (size == 0u ? 0u : ((1u << size) - 1u)); const std::uint32_t destination_mask = source_mask << decoded.sa; const auto destination = constants[decoded.rt]; set_constant_and_seed(constants, decoded.rt, lhs && destination ? std::optional((*destination & ~destination_mask) | ((*lhs & source_mask) << decoded.sa)) : std::nullopt, seeds, ranges); break; } case OpcodeKind::Slt: case OpcodeKind::Sltu: case OpcodeKind::Mfhi: case OpcodeKind::Mflo: set_constant_and_seed(constants, decoded.rd, std::nullopt, seeds, ranges); break; case OpcodeKind::Lw: case OpcodeKind::Lwl: case OpcodeKind::Lwr: case OpcodeKind::Lh: case OpcodeKind::Lhu: case OpcodeKind::Lb: case OpcodeKind::Lbu: case OpcodeKind::Lwc1: case OpcodeKind::Mfc1: case OpcodeKind::Cfc1: case OpcodeKind::Mfv: set_constant_and_seed(constants, decoded.rt, std::nullopt, seeds, ranges); break; case OpcodeKind::Jal: case OpcodeKind::Bltzal: case OpcodeKind::Bgezal: case OpcodeKind::Bltzall: case OpcodeKind::Bgezall: constants[31] = pc + 8u; break; case OpcodeKind::Jalr: if (decoded.rd != 0u) constants[decoded.rd] = pc + 8u; break; case OpcodeKind::Unsupported: case OpcodeKind::Vfpu: case OpcodeKind::Syscall: clear_all_constants(constants); break; default: break; } constants[0] = 0u; } void collect_materialized_code_pointers(const GuestMemory &memory, const std::vector &ranges, std::map &seeds) { const auto block_starts = collect_global_block_starts(memory, ranges); for (const auto block_start : block_starts) { if (!is_executable_address(ranges, block_start)) continue; ConstantState constants{}; clear_all_constants(constants); std::uint32_t pc = block_start; while (is_executable_address(ranges, pc)) { if (pc != block_start && block_starts.contains(pc)) break; const auto decoded = decode_allegrex(memory.load32(pc)); propagate_constant(decoded, pc, constants, seeds, ranges); if (decoded.has_delay_slot()) { const auto slot = decode_allegrex(memory.load32(pc + 4u)); propagate_constant(slot, pc + 4u, constants, seeds, ranges); break; } if (decoded.is_control_flow()) break; pc += 4u; } } // Function pointers are frequently assembled immediately before an API call, // including in the call's delay slot. A global block boundary can split the // LUI from that final ADDIU/ORI when the surrounding code has overlapping // entry points. Scan a short straight-line window from every LUI as a // conservative, architecture-wide supplement. Only values inside executable // ranges become seeds, so data addresses and ordinary constants are ignored. for (const auto &range : ranges) { for (std::uint32_t start = range.start; start + 4u <= range.end; start += 4u) { const auto first = decode_allegrex(memory.load32(start)); if (first.kind != OpcodeKind::Lui || first.rt == 0u) continue; ConstantState constants{}; clear_all_constants(constants); std::uint32_t pc = start; for (std::size_t count = 0; count < 24u && is_executable_address(ranges, pc); ++count) { const auto decoded = decode_allegrex(memory.load32(pc)); propagate_constant(decoded, pc, constants, seeds, ranges); if (decoded.has_delay_slot()) { if (is_executable_address(ranges, pc + 4u)) { const auto slot = decode_allegrex(memory.load32(pc + 4u)); propagate_constant(slot, pc + 4u, constants, seeds, ranges); } break; } if (decoded.is_control_flow()) break; pc += 4u; } } } } void collect_relocated_data_code_pointers(const Elf32Image &elf, const GuestMemory &memory, std::uint32_t load_base, const std::vector &ranges, std::map &seeds) { for (std::size_t i = 0; i < elf.segments().size(); ++i) { const auto &segment = elf.segments()[i]; if (segment.type != 1u || (segment.flags & 1u) != 0u || segment.file_size < 4u) continue; const std::uint32_t start = elf.segment_runtime_address(i, load_base); const std::uint32_t size = segment.file_size & ~3u; for (std::uint32_t offset = 0u; offset < size; offset += 4u) { add_seed(seeds, ranges, memory.load32(start + offset), "relocated_data_code_pointer"); } } } std::map collect_initial_seeds(const Elf32Image &elf, const GuestMemory &memory, std::uint32_t load_base, const std::vector &ranges) { std::map seeds; const std::uint32_t entry = elf.runtime_entry(load_base); add_seed(seeds, ranges, entry, "elf_entry"); for (const auto &range : ranges) { for (std::uint32_t pc = range.start; pc + 4u <= range.end; pc += 4u) { const auto decoded = decode_allegrex(memory.load32(pc)); if (decoded.kind != OpcodeKind::Jal) continue; add_seed(seeds, ranges, direct_jump_target(pc, decoded), "direct_jal_target"); } } collect_materialized_code_pointers(memory, ranges, seeds); collect_relocated_data_code_pointers(elf, memory, load_base, ranges, seeds); // R_MIPS_32 relocations are the authoritative source for function pointers // stored in read-only tables embedded in the executable segment (init arrays, // vtables, callbacks). Scanning raw RX words would confuse J opcodes with pointers. for (const auto &site : elf.relocation_sites(load_base)) { if (site.type != 2u || !memory.contains(site.patch_address, 4u)) continue; add_seed(seeds, ranges, memory.load32(site.patch_address), "relocated_r_mips32_code_pointer"); } return seeds; } FunctionAnalysis analyze_function(std::uint32_t entry, const GuestMemory &memory, const std::vector &ranges, const std::map &known_seeds, std::size_t max_instructions) { FunctionAnalysis result{}; result.entry = entry; std::deque pending_blocks; std::set queued_blocks; pending_blocks.push_back(entry); queued_blocks.insert(entry); while (!pending_blocks.empty()) { const std::uint32_t block_start = pending_blocks.front(); pending_blocks.pop_front(); if (!is_executable_address(ranges, block_start)) continue; const bool new_entry = result.entry_labels.insert(block_start).second; if (new_entry) ++result.basic_block_count; // A branch target may have already been decoded linearly from another block. // It still must remain a dispatcher/basic-block entry. if (result.labels.contains(block_start)) continue; std::uint32_t pc = block_start; while (is_executable_address(ranges, pc)) { if (result.labels.size() >= max_instructions) { result.truncated = true; return result; } if (result.labels.contains(pc)) break; if (pc != entry && known_seeds.contains(pc) && pc != block_start) break; result.labels.insert(pc); const auto decoded = decode_allegrex(memory.load32(pc)); if (decoded.kind == OpcodeKind::Unsupported || decoded.kind == OpcodeKind::Vfpu) { ++result.unsupported_instruction_count; } if (is_conditional_branch(decoded.kind)) { const std::uint32_t taken = branch_target(pc, decoded); const std::uint32_t fallthrough = pc + 8u; if (is_executable_address(ranges, taken) && queued_blocks.insert(taken).second) pending_blocks.push_back(taken); if (is_executable_address(ranges, fallthrough) && queued_blocks.insert(fallthrough).second) pending_blocks.push_back(fallthrough); break; } if (decoded.kind == OpcodeKind::J) { const std::uint32_t target = direct_jump_target(pc, decoded); if (is_executable_address(ranges, target)) { if (target == entry || !known_seeds.contains(target)) { if (queued_blocks.insert(target).second) pending_blocks.push_back(target); } else { result.direct_calls.insert(target); // tail call to another known entry. } } break; } if (decoded.kind == OpcodeKind::Jal) { const std::uint32_t target = direct_jump_target(pc, decoded); if (is_executable_address(ranges, target)) result.direct_calls.insert(target); const std::uint32_t continuation = pc + 8u; if (is_executable_address(ranges, continuation) && queued_blocks.insert(continuation).second) { pending_blocks.push_back(continuation); } break; } if (decoded.kind == OpcodeKind::Jalr) { result.indirect_call_sites.insert(pc); const std::uint32_t continuation = pc + 8u; if (is_executable_address(ranges, continuation) && queued_blocks.insert(continuation).second) { pending_blocks.push_back(continuation); } break; } if (decoded.kind == OpcodeKind::Jr) break; pc += decoded.has_delay_slot() ? 8u : 4u; } } return result; } } // namespace bool is_executable_address(const std::vector &ranges, std::uint32_t address) noexcept { const auto it = std::upper_bound(ranges.begin(), ranges.end(), address, [](std::uint32_t value, const ExecutableRange &range) { return value < range.start; }); if (it == ranges.begin()) return false; const auto &range = *std::prev(it); return address >= range.start && address < range.end && (address & 3u) == 0u; } ProgramAnalysis analyze_program(const Elf32Image &elf, const GuestMemory &memory, std::uint32_t load_base, std::size_t max_instructions_per_function) { ProgramAnalysis program{}; program.executable_ranges = executable_ranges_for(elf, load_base); program.seeds = collect_initial_seeds(elf, memory, load_base, program.executable_ranges); program.functions.reserve(program.seeds.size()); std::unordered_map label_owners; for (const auto &[entry, source] : program.seeds) { (void)source; auto function = analyze_function(entry, memory, program.executable_ranges, program.seeds, max_instructions_per_function); for (const auto label : function.labels) program.covered_labels.insert(label); for (const auto label : function.entry_labels) { const auto [it, inserted] = label_owners.emplace(label, program.functions.size()); if (!inserted && it->second != program.functions.size()) ++program.overlapping_label_count; program.covered_entry_labels.insert(label); } program.functions.push_back(std::move(function)); } return program; } } // namespace psprecomp