rustfmt, then clippy -D warnings across the three new crates. Mechanical,
except three decisions that are stated rather than silently allowed:
* lzx.rs gets file-scoped needless_range_loop/explicit_counter_loop allows.
Index arithmetic IS the algorithm -- LZX is defined over symbol indices,
Huffman slots and window positions, and a decompressor that is merely
idiomatic is worth nothing if it is not bit-exact.
* sylpheed-xexdb gets crate-scoped allows for needless_range_loop (nine
sites index reg[r] where r is the PowerPC register number -- the index is
the meaning), too_many_arguments and type_complexity. This code arrived
whole from a retired repository; a refactor here would be an unreviewed
edit dressed as a lint fix.
* Everything else clippy asked for is FIXED, including all 14 doc-indent
sites, the let-else, and a Prepared type alias in the binary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
805 lines
30 KiB
Rust
805 lines
30 KiB
Rust
//! Function boundary detection via PPC prologue/epilogue pattern matching.
|
||
//!
|
||
//! Strategy (multi-pass):
|
||
//! 1. Identify all `bl` (branch-and-link) targets — these are call sites,
|
||
//! hence very likely function entry points.
|
||
//! 2. Scan the save/restore GPR helper region and label it.
|
||
//! 3. For each candidate entry, look for prologue patterns:
|
||
//! a) `mfspr rN, LR` (typically r0 or r12)
|
||
//! b) `bl __savegprlr_NN` (call into save stub)
|
||
//! c) `stwu r1, -N(r1)` (allocate stack frame)
|
||
//! If a prologue is confirmed, record the function and its stack frame size.
|
||
//! 4. Walk forward from each function entry to find the epilogue:
|
||
//! a) `blr` (return)
|
||
//! b) `b __restgprlr_NN` (tail-branch into restore stub which returns)
|
||
//! Mark the function's end address.
|
||
//! 5. Detect leaf functions: `bl` targets that lack a prologue but eventually `blr`.
|
||
|
||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||
|
||
/// Information about a detected function.
|
||
#[derive(Debug, Clone)]
|
||
pub struct FuncInfo {
|
||
/// Absolute start address.
|
||
pub start: u32,
|
||
/// Absolute end address (exclusive — one past last instruction).
|
||
pub end: u32,
|
||
/// Stack frame size (0 if unknown / leaf).
|
||
pub frame_size: u32,
|
||
/// Number of saved GPRs (via __savegprlr helper), 0 if unknown.
|
||
pub saved_gprs: u32,
|
||
/// True if this is a leaf function (no bl, no frame setup).
|
||
pub is_leaf: bool,
|
||
/// True if this is a save/restore GPR helper stub.
|
||
pub is_saverestore: bool,
|
||
/// True if `.pdata` has a RUNTIME_FUNCTION whose `BeginAddress` matches `start`.
|
||
/// Authoritative ground truth from the linker; rows without this flag are
|
||
/// prologue-detected only and may carry boundary errors.
|
||
pub pdata_validated: bool,
|
||
/// Function size in bytes per `.pdata`'s `function_length` field, if known.
|
||
/// Absent (None) when this row is prologue-only.
|
||
pub pdata_length: Option<u32>,
|
||
/// Prolog size in bytes per `.pdata`'s `prolog_length` field, if known.
|
||
/// The linker's own count — more reliable than the prologue pattern match.
|
||
pub pdata_prolog_length: Option<u32>,
|
||
/// True when `.pdata`'s exception-flag bit is set on this entry — the
|
||
/// function has a registered C++ EH (or SEH) frame handler. Always false
|
||
/// for entries without `.pdata` coverage. (M9)
|
||
pub has_eh: bool,
|
||
}
|
||
|
||
/// Result of the function analysis pass.
|
||
pub struct FuncAnalysis {
|
||
/// address → FuncInfo for every detected function, sorted by address.
|
||
pub functions: BTreeMap<u32, FuncInfo>,
|
||
/// Addresses in the save-GPR region (start of __savegprlr block).
|
||
pub save_gpr_base: Option<u32>,
|
||
/// Addresses in the restore-GPR region (start of __restgprlr block).
|
||
pub restore_gpr_base: Option<u32>,
|
||
/// Raw `.pdata` entries from the binary, in original order. Empty when no
|
||
/// `.pdata` was supplied. Mirrored into the DB as `pdata_entries`.
|
||
pub pdata_entries: Vec<sylpheed_xex::pdata::PdataEntry>,
|
||
}
|
||
|
||
// ── Instruction field helpers ──────────────────────────────────────────────
|
||
|
||
fn op(instr: u32) -> u32 {
|
||
(instr >> 26) & 0x3F
|
||
}
|
||
fn bits(instr: u32, hi: u32, lo: u32) -> u32 {
|
||
(instr >> (31 - hi)) & ((1 << (hi - lo + 1)) - 1)
|
||
}
|
||
|
||
fn is_mfspr_lr(instr: u32) -> Option<u32> {
|
||
// mfspr rD, LR → opcode 31, xo=339, spr=8
|
||
if op(instr) != 31 {
|
||
return None;
|
||
}
|
||
let xo = bits(instr, 30, 21);
|
||
if xo != 339 {
|
||
return None;
|
||
}
|
||
let spr = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11);
|
||
if spr != 8 {
|
||
return None;
|
||
}
|
||
Some(bits(instr, 10, 6)) // return rD
|
||
}
|
||
|
||
#[allow(dead_code)]
|
||
fn is_mtspr_lr(instr: u32) -> bool {
|
||
// mtspr LR, rS → opcode 31, xo=467, spr=8
|
||
if op(instr) != 31 {
|
||
return false;
|
||
}
|
||
let xo = bits(instr, 30, 21);
|
||
if xo != 467 {
|
||
return false;
|
||
}
|
||
let spr = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11);
|
||
spr == 8
|
||
}
|
||
|
||
fn is_stwu_r1(instr: u32) -> Option<i32> {
|
||
// stwu r1, d(r1) → opcode 37, rS=1, rA=1
|
||
if op(instr) != 37 {
|
||
return None;
|
||
}
|
||
let rs = bits(instr, 10, 6);
|
||
let ra = bits(instr, 15, 11);
|
||
if rs != 1 || ra != 1 {
|
||
return None;
|
||
}
|
||
let d = ((instr & 0xFFFF) as i16) as i32;
|
||
Some(d) // negative = frame allocation
|
||
}
|
||
|
||
fn is_blr(instr: u32) -> bool {
|
||
instr == 0x4E800020
|
||
}
|
||
|
||
fn is_bctr(instr: u32) -> bool {
|
||
instr == 0x4E800420
|
||
}
|
||
|
||
fn is_bl(instr: u32) -> Option<u32> {
|
||
// bl target → opcode 18, LK=1, AA=0
|
||
if op(instr) != 18 {
|
||
return None;
|
||
}
|
||
if instr & 1 == 0 {
|
||
return None;
|
||
} // must have LK bit
|
||
if instr & 2 != 0 {
|
||
return None;
|
||
} // not absolute
|
||
// Return the signed offset
|
||
let li = instr & 0x03FFFFFC;
|
||
Some(li)
|
||
}
|
||
|
||
fn is_b(instr: u32) -> Option<u32> {
|
||
// b target → opcode 18, LK=0, AA=0
|
||
if op(instr) != 18 {
|
||
return None;
|
||
}
|
||
if instr & 1 != 0 {
|
||
return None;
|
||
} // no LK bit
|
||
if instr & 2 != 0 {
|
||
return None;
|
||
} // not absolute
|
||
Some(instr & 0x03FFFFFC)
|
||
}
|
||
|
||
fn sign_ext26(val: u32) -> i32 {
|
||
((val << 6) as i32) >> 6
|
||
}
|
||
|
||
fn bl_target(instr: u32, addr: u32) -> Option<u32> {
|
||
is_bl(instr).map(|off| addr.wrapping_add(sign_ext26(off) as u32))
|
||
}
|
||
|
||
fn b_target(instr: u32, addr: u32) -> Option<u32> {
|
||
is_b(instr).map(|off| addr.wrapping_add(sign_ext26(off) as u32))
|
||
}
|
||
|
||
// ── Read instruction from PE ───────────────────────────────────────────────
|
||
|
||
fn read_instr(pe: &[u8], abs_addr: u32, image_base: u32) -> Option<u32> {
|
||
let off = abs_addr.wrapping_sub(image_base) as usize;
|
||
if off + 4 > pe.len() {
|
||
return None;
|
||
}
|
||
Some(u32::from_be_bytes([
|
||
pe[off],
|
||
pe[off + 1],
|
||
pe[off + 2],
|
||
pe[off + 3],
|
||
]))
|
||
}
|
||
|
||
// ── Detect the save/restore GPR helper stubs ───────────────────────────────
|
||
//
|
||
// These are a well-known pattern emitted by the Xbox 360 linker.
|
||
// Save block: a cascade of `std rN, offset(r1)` for r14..r31 + `stw r12, -8(r1)` + `blr`
|
||
// Restore: a cascade of `ld rN, offset(r1)` for r14..r31 + `lwz r12, -8(r1)` + `mtspr LR, r12` + `blr`
|
||
//
|
||
// We detect the save block by finding 18 consecutive `std rN, ...(r1)` instructions
|
||
// for r14 through r31.
|
||
|
||
fn find_saverestore_stubs(
|
||
pe: &[u8],
|
||
image_base: u32,
|
||
code_ranges: &[(u32, u32)], // (abs_start, abs_end)
|
||
) -> (Option<u32>, Option<u32>) {
|
||
let mut save_base = None;
|
||
let mut restore_base = None;
|
||
|
||
for &(start, end) in code_ranges {
|
||
let mut addr = start;
|
||
while addr + 4 * 18 < end {
|
||
// Check if this is `std r14, ...(r1)` — opcode 62 (std), rS=14, rA=1
|
||
let instr = match read_instr(pe, addr, image_base) {
|
||
Some(i) => i,
|
||
None => {
|
||
addr += 4;
|
||
continue;
|
||
}
|
||
};
|
||
if op(instr) == 62
|
||
&& bits(instr, 10, 6) == 14
|
||
&& bits(instr, 15, 11) == 1
|
||
&& (instr & 3) == 0
|
||
{
|
||
// Verify it's a cascade: r14, r15, ..., r31
|
||
let mut ok = true;
|
||
for i in 0u32..18 {
|
||
let check = match read_instr(pe, addr + i * 4, image_base) {
|
||
Some(c) => c,
|
||
None => {
|
||
ok = false;
|
||
break;
|
||
}
|
||
};
|
||
if op(check) != 62 || bits(check, 10, 6) != 14 + i || bits(check, 15, 11) != 1 {
|
||
ok = false;
|
||
break;
|
||
}
|
||
}
|
||
if ok {
|
||
save_base = Some(addr);
|
||
// Restore block typically follows the save block
|
||
// After save: stw r12, -8(r1) + blr, then restore starts
|
||
let after_save = addr + 18 * 4 + 8; // skip stw r12 + blr
|
||
let check = read_instr(pe, after_save, image_base);
|
||
if let Some(c) = check {
|
||
// Should be `ld r14, ...(r1)` — opcode 58, rT=14, rA=1
|
||
if op(c) == 58 && bits(c, 10, 6) == 14 && bits(c, 15, 11) == 1 {
|
||
restore_base = Some(after_save);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
addr += 4;
|
||
}
|
||
if save_base.is_some() {
|
||
break;
|
||
}
|
||
}
|
||
|
||
(save_base, restore_base)
|
||
}
|
||
|
||
// ── Main analysis ──────────────────────────────────────────────────────────
|
||
|
||
#[tracing::instrument(skip_all, fields(image_base = format_args!("{:#010x}", image_base), entry_point = format_args!("{:#010x}", entry_point)))]
|
||
pub fn analyze(
|
||
pe: &[u8],
|
||
image_base: u32,
|
||
entry_point: u32,
|
||
code_sections: &[(u32, u32, u32)], // (va_start, va_size, flags)
|
||
) -> FuncAnalysis {
|
||
analyze_with_pdata(pe, image_base, entry_point, code_sections, &[])
|
||
}
|
||
|
||
/// Same as [`analyze`] but also unions `.pdata` `RUNTIME_FUNCTION` entries
|
||
/// into the candidate set. Each surviving function carries `pdata_validated`
|
||
/// when its start matches a pdata `BeginAddress`, and `pdata_length` when
|
||
/// the linker-supplied length disagrees with the prologue walk.
|
||
///
|
||
/// Pdata entries that have no prologue match (orphans) are still emitted,
|
||
/// using the linker-supplied length to bound the function.
|
||
///
|
||
/// What this layer does NOT do:
|
||
/// - Does not edit the `prolog_length` we'd derive from prologue analysis;
|
||
/// `frame_size` and `saved_gprs` remain best-effort prologue inferences.
|
||
/// - Does not infer base/derived call edges — that's M3+M5.
|
||
/// - Does not discover functions that are neither in `.pdata` nor the target of
|
||
/// a `bl`. Some code does live in the `.pdata` gaps — small leaf helpers
|
||
/// reached only through a function-pointer table. Measured against a Ghidra
|
||
/// export of the reference title, 217 such entries exist that this pass does
|
||
/// not emit. Two obvious heuristics for them were evaluated and **rejected**:
|
||
/// "a data word that points into code outside any `.pdata` range" yields 1994
|
||
/// new candidates of which Ghidra confirms 37, and "8-byte-aligned word in a
|
||
/// gap, preceded by `blr` + padding" yields 4011 of which Ghidra confirms
|
||
/// 106. Either would flood `functions` with several thousand unvalidated
|
||
/// rows and destroy the property that every emitted boundary is exact, in
|
||
/// exchange for a couple of hundred real ones. If this gap needs closing, it
|
||
/// wants a real recursive-descent walk seeded from the function-pointer
|
||
/// tables, not a pattern match.
|
||
#[tracing::instrument(skip_all, fields(image_base = format_args!("{:#010x}", image_base), entry_point = format_args!("{:#010x}", entry_point), pdata_entries = pdata.len()))]
|
||
pub fn analyze_with_pdata(
|
||
pe: &[u8],
|
||
image_base: u32,
|
||
entry_point: u32,
|
||
code_sections: &[(u32, u32, u32)],
|
||
pdata: &[sylpheed_xex::pdata::PdataEntry],
|
||
) -> FuncAnalysis {
|
||
let started = std::time::Instant::now();
|
||
let code_ranges: Vec<(u32, u32)> = code_sections
|
||
.iter()
|
||
.map(|(va, sz, _)| (image_base + va, image_base + va + sz))
|
||
.collect();
|
||
|
||
// 1. Find save/restore stubs
|
||
let (save_base, restore_base) = find_saverestore_stubs(pe, image_base, &code_ranges);
|
||
if let Some(sb) = save_base {
|
||
tracing::debug!(addr = format_args!("{:#010x}", sb), "__savegprlr stub");
|
||
}
|
||
if let Some(rb) = restore_base {
|
||
tracing::debug!(addr = format_args!("{:#010x}", rb), "__restgprlr stub");
|
||
}
|
||
|
||
// Set of addresses in the save/restore region (to exclude from function detection)
|
||
let mut saverestore_addrs: HashSet<u32> = HashSet::new();
|
||
if let Some(sb) = save_base {
|
||
// Save block: 18 std + stw + blr = 20 instructions
|
||
for i in 0..20 {
|
||
saverestore_addrs.insert(sb + i * 4);
|
||
}
|
||
}
|
||
if let Some(rb) = restore_base {
|
||
// Restore block: 18 ld + lwz + mtspr + blr = 21 instructions
|
||
for i in 0..21 {
|
||
saverestore_addrs.insert(rb + i * 4);
|
||
}
|
||
}
|
||
|
||
// 2. Collect all bl targets as candidate function entries.
|
||
// Union: bl targets ∪ pdata BeginAddresses ∪ entry_point.
|
||
let mut call_targets: HashSet<u32> = HashSet::new();
|
||
call_targets.insert(entry_point);
|
||
|
||
for &(start, end) in &code_ranges {
|
||
let mut addr = start;
|
||
while addr < end {
|
||
if let Some(instr) = read_instr(pe, addr, image_base)
|
||
&& let Some(target) = bl_target(instr, addr)
|
||
{
|
||
// Don't count calls into save/restore stubs as function entries
|
||
if !saverestore_addrs.contains(&target) {
|
||
call_targets.insert(target);
|
||
}
|
||
}
|
||
addr += 4;
|
||
}
|
||
}
|
||
|
||
// Index pdata by begin_address for O(1) prologue → length lookup.
|
||
let pdata_by_begin: HashMap<u32, &sylpheed_xex::pdata::PdataEntry> =
|
||
pdata.iter().map(|e| (e.begin_address, e)).collect();
|
||
for e in pdata {
|
||
if !saverestore_addrs.contains(&e.begin_address) {
|
||
call_targets.insert(e.begin_address);
|
||
}
|
||
}
|
||
|
||
// Tail-call targets.
|
||
//
|
||
// `bl ∪ pdata` misses a function that is only ever entered by a tail call:
|
||
// it has no `bl` site, and small frameless helpers are frequently absent
|
||
// from `.pdata`. `0x82169630` in the reference title is one — it follows a
|
||
// `b 0x825F0FDC` that ends the previous function and is itself reached only
|
||
// by `b`, so nothing in the union nominates it.
|
||
//
|
||
// `.pdata` makes the test exact: a non-linking `b` whose target leaves the
|
||
// source's own linker-declared range, and that does not land inside any
|
||
// other declared range, is entering a *different* function — not branching
|
||
// within this one. Intra-function jumps and switch arms both stay inside
|
||
// the range and are therefore never nominated.
|
||
let pdata_sorted: Vec<(u32, u32)> = {
|
||
let mut v: Vec<(u32, u32)> = pdata
|
||
.iter()
|
||
.map(|e| (e.begin_address, e.end_address()))
|
||
.collect();
|
||
v.sort_unstable();
|
||
v
|
||
};
|
||
let containing = |addr: u32| -> Option<(u32, u32)> {
|
||
match pdata_sorted.binary_search_by_key(&addr, |&(b, _)| b) {
|
||
Ok(i) => Some(pdata_sorted[i]),
|
||
Err(0) => None,
|
||
Err(i) => {
|
||
let (b, e) = pdata_sorted[i - 1];
|
||
(addr < e).then_some((b, e))
|
||
}
|
||
}
|
||
};
|
||
//
|
||
// `.pdata` does not cover the whole of `.text` here — roughly 450 KB of
|
||
// code sits in gaps between declared ranges, and both ends of a tail call
|
||
// can land there. When the source has no declared range to compare
|
||
// against, fall back on the standard entry test: the target is a function
|
||
// start if the instruction *before* it ends a function (`blr`, `bctr`, or
|
||
// an unconditional `b`). Code placed immediately after a terminator is
|
||
// unreachable by fallthrough, so something must enter it there.
|
||
let ends_function = |addr: u32| -> bool {
|
||
match read_instr(pe, addr, image_base) {
|
||
Some(i) => is_blr(i) || is_bctr(i) || is_b(i).is_some(),
|
||
None => false,
|
||
}
|
||
};
|
||
let mut tail_call_targets = 0usize;
|
||
for &(start, end) in &code_ranges {
|
||
let mut addr = start;
|
||
while addr < end {
|
||
if let Some(instr) = read_instr(pe, addr, image_base)
|
||
&& let Some(target) = b_target(instr, addr)
|
||
&& !saverestore_addrs.contains(&target)
|
||
&& containing(target).is_none()
|
||
&& code_ranges.iter().any(|&(s, e)| target >= s && target < e)
|
||
&& match containing(addr) {
|
||
// Source is declared: a jump out of its own range is a
|
||
// tail call, one inside it is ordinary control flow.
|
||
Some((src_lo, src_hi)) => target < src_lo || target >= src_hi,
|
||
// Source is in an undeclared gap: fall back to the
|
||
// preceding-terminator test.
|
||
None => target >= 4 && ends_function(target - 4),
|
||
}
|
||
&& call_targets.insert(target)
|
||
{
|
||
tail_call_targets += 1;
|
||
}
|
||
addr += 4;
|
||
}
|
||
}
|
||
|
||
tracing::debug!(
|
||
candidates = call_targets.len(),
|
||
pdata_entries = pdata.len(),
|
||
tail_call_targets,
|
||
"function candidates (bl ∪ pdata ∪ tail-call)"
|
||
);
|
||
|
||
// 3. For each candidate, detect prologue and walk to epilogue. Pdata
|
||
// metadata is layered on after the prologue walk so a missing prologue
|
||
// still yields an entry when pdata covers it.
|
||
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
|
||
|
||
for &func_addr in &call_targets {
|
||
let pdata_entry = pdata_by_begin.get(&func_addr).copied();
|
||
|
||
if let Some(mut fi) = analyze_function(
|
||
pe,
|
||
image_base,
|
||
func_addr,
|
||
&code_ranges,
|
||
save_base,
|
||
restore_base,
|
||
) {
|
||
if let Some(p) = pdata_entry {
|
||
fi.pdata_validated = true;
|
||
fi.pdata_length = Some(p.function_length);
|
||
fi.pdata_prolog_length = Some(p.prolog_length);
|
||
// `flags` bit 1 mirrors packed-word bit 31 = exception handler
|
||
// registered (see `sylpheed_xex::pdata`). Bit 0 is the 32-bit-code
|
||
// flag, which is set on essentially every PPC entry.
|
||
fi.has_eh = (p.flags & 0x2) != 0;
|
||
// The linker's length is ground truth in BOTH directions: a
|
||
// prologue walk that ran past a `blr` into the next function is
|
||
// just as wrong as one that stopped early. Only a zero-length
|
||
// entry (never observed, but cheap to guard) falls back.
|
||
if p.function_length > 0 {
|
||
fi.end = p.begin_address.wrapping_add(p.function_length);
|
||
}
|
||
}
|
||
functions.insert(func_addr, fi);
|
||
} else if let Some(p) = pdata_entry {
|
||
// Orphan: pdata claims a function here but no prologue matched.
|
||
// Emit a synthetic entry so the row exists for downstream queries.
|
||
let end = p.begin_address.wrapping_add(p.function_length);
|
||
functions.insert(
|
||
func_addr,
|
||
FuncInfo {
|
||
start: func_addr,
|
||
end,
|
||
frame_size: 0,
|
||
saved_gprs: 0,
|
||
// A pdata orphan is usually a hand-written or fully inlined
|
||
// leaf; decide it from the body rather than guessing.
|
||
is_leaf: !range_has_call(pe, image_base, func_addr, end),
|
||
is_saverestore: false,
|
||
pdata_validated: true,
|
||
pdata_length: Some(p.function_length),
|
||
pdata_prolog_length: Some(p.prolog_length),
|
||
has_eh: (p.flags & 0x2) != 0,
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
// 4. Label save/restore stubs as special functions — one entry for the whole block
|
||
if let Some(sb) = save_base {
|
||
// The save block is one cascade: entry at each rN, falls through to blr
|
||
// Treat as a single function with the first entry point
|
||
let pe_sb = pdata_by_begin.get(&sb).copied();
|
||
functions.insert(
|
||
sb,
|
||
FuncInfo {
|
||
start: sb,
|
||
end: sb + 20 * 4, // 18 std + stw r12 + blr
|
||
frame_size: 0,
|
||
saved_gprs: 18,
|
||
is_leaf: true,
|
||
is_saverestore: true,
|
||
pdata_validated: pe_sb.is_some(),
|
||
pdata_length: pe_sb.map(|p| p.function_length),
|
||
pdata_prolog_length: pe_sb.map(|p| p.prolog_length),
|
||
has_eh: pe_sb.map(|p| (p.flags & 0x2) != 0).unwrap_or(false),
|
||
},
|
||
);
|
||
}
|
||
if let Some(rb) = restore_base {
|
||
let pe_rb = pdata_by_begin.get(&rb).copied();
|
||
functions.insert(
|
||
rb,
|
||
FuncInfo {
|
||
start: rb,
|
||
end: rb + 21 * 4, // 18 ld + lwz r12 + mtspr LR + blr
|
||
frame_size: 0,
|
||
saved_gprs: 18,
|
||
is_leaf: true,
|
||
is_saverestore: true,
|
||
pdata_validated: pe_rb.is_some(),
|
||
pdata_length: pe_rb.map(|p| p.function_length),
|
||
pdata_prolog_length: pe_rb.map(|p| p.prolog_length),
|
||
has_eh: pe_rb.map(|p| (p.flags & 0x2) != 0).unwrap_or(false),
|
||
},
|
||
);
|
||
}
|
||
|
||
// 5. Reconcile candidate starts against the linker's ground truth.
|
||
//
|
||
// 5a. A `bl` whose target lands *strictly inside* a `.pdata`-validated
|
||
// function is not a second function — it is a branch into the middle
|
||
// of one (shared epilogue, computed-goto landing pad, or a
|
||
// mis-decoded word). Left in place such a candidate would truncate
|
||
// the real function at step 5b and orphan the rest of its body.
|
||
// The ranges come straight from `.pdata`, which the linker emits
|
||
// sorted and non-overlapping — the property the binary search needs.
|
||
// (Deriving them from `functions` instead would fold in the
|
||
// save/restore stub rows, whose `end` is a fixed block length rather
|
||
// than a pdata length and can therefore overlap a neighbour.)
|
||
let pdata_ranges: Vec<(u32, u32)> = pdata
|
||
.iter()
|
||
.filter(|e| e.function_length > 0)
|
||
.map(|e| (e.begin_address, e.end_address()))
|
||
.collect();
|
||
debug_assert!(pdata_ranges.windows(2).all(|w| w[0].1 <= w[1].0));
|
||
let interior: Vec<u32> = functions
|
||
.iter()
|
||
.filter(|(_, f)| !f.pdata_validated)
|
||
.map(|(&addr, _)| addr)
|
||
.filter(|&addr| {
|
||
pdata_ranges
|
||
.binary_search_by(|&(s, e)| {
|
||
if addr < s {
|
||
std::cmp::Ordering::Greater
|
||
} else if addr >= e {
|
||
std::cmp::Ordering::Less
|
||
} else {
|
||
std::cmp::Ordering::Equal
|
||
}
|
||
})
|
||
.is_ok()
|
||
})
|
||
.collect();
|
||
let interior_dropped = interior.len();
|
||
for addr in interior {
|
||
functions.remove(&addr);
|
||
}
|
||
|
||
// 5b. Trim overlaps that remain. Only prologue-only rows are trimmed —
|
||
// a `.pdata` length is authoritative and must survive intact even
|
||
// when a neighbouring heuristic row disagrees.
|
||
let starts: Vec<u32> = functions.keys().copied().collect();
|
||
for i in 0..starts.len().saturating_sub(1) {
|
||
let cur = starts[i];
|
||
let next = starts[i + 1];
|
||
if let Some(fi) = functions.get_mut(&cur)
|
||
&& !fi.pdata_validated
|
||
&& fi.end > next
|
||
{
|
||
fi.end = next;
|
||
}
|
||
}
|
||
|
||
let elapsed_ms = started.elapsed().as_millis() as f64;
|
||
metrics::histogram!("analysis.phase_ms", "phase" => "functions").record(elapsed_ms);
|
||
let pdata_validated_count = functions.values().filter(|f| f.pdata_validated).count();
|
||
tracing::info!(
|
||
functions = functions.len(),
|
||
pdata_entries = pdata.len(),
|
||
pdata_validated = pdata_validated_count,
|
||
interior_candidates_dropped = interior_dropped,
|
||
elapsed_ms,
|
||
"function detection complete"
|
||
);
|
||
|
||
FuncAnalysis {
|
||
functions,
|
||
save_gpr_base: save_base,
|
||
restore_gpr_base: restore_base,
|
||
pdata_entries: pdata.to_vec(),
|
||
}
|
||
}
|
||
|
||
/// True when `[start, end)` contains any linking branch — `bl`, `bcl`,
|
||
/// `bctrl` or `bclrl`. Used to classify pdata-only entries as leaf or not.
|
||
fn range_has_call(pe: &[u8], image_base: u32, start: u32, end: u32) -> bool {
|
||
let mut addr = start;
|
||
while addr < end {
|
||
let Some(instr) = read_instr(pe, addr, image_base) else {
|
||
return false;
|
||
};
|
||
let opcode = op(instr);
|
||
// I-form / B-form with LK, and XL-form bclrl / bcctrl.
|
||
if (opcode == 18 || opcode == 16) && instr & 1 == 1 {
|
||
return true;
|
||
}
|
||
if opcode == 19 && instr & 1 == 1 && matches!(bits(instr, 30, 21), 16 | 528) {
|
||
return true;
|
||
}
|
||
addr = addr.wrapping_add(4);
|
||
}
|
||
false
|
||
}
|
||
|
||
/// Analyze a single function starting at `func_addr`.
|
||
fn analyze_function(
|
||
pe: &[u8],
|
||
image_base: u32,
|
||
func_addr: u32,
|
||
code_ranges: &[(u32, u32)],
|
||
save_base: Option<u32>,
|
||
restore_base: Option<u32>,
|
||
) -> Option<FuncInfo> {
|
||
// Verify the address is within a code section
|
||
let in_code = code_ranges
|
||
.iter()
|
||
.any(|&(s, e)| func_addr >= s && func_addr < e);
|
||
if !in_code {
|
||
return None;
|
||
}
|
||
|
||
let instr0 = read_instr(pe, func_addr, image_base)?;
|
||
|
||
let mut frame_size: u32 = 0;
|
||
let mut saved_gprs: u32 = 0;
|
||
let mut is_leaf = false;
|
||
let mut prologue_len: u32 = 0;
|
||
|
||
// Pattern A: mfspr rN, LR [+ bl __savegprlr_NN] + stwu r1, -N(r1)
|
||
if let Some(_lr_reg) = is_mfspr_lr(instr0) {
|
||
prologue_len = 4;
|
||
let instr1 = read_instr(pe, func_addr + 4, image_base).unwrap_or(0);
|
||
|
||
// Check if next is bl to save stub
|
||
if let Some(target) = bl_target(instr1, func_addr + 4)
|
||
&& let Some(sb) = save_base
|
||
&& target >= sb
|
||
&& target < sb + 18 * 4
|
||
{
|
||
let idx = (target - sb) / 4;
|
||
saved_gprs = 18 - idx;
|
||
prologue_len = 8;
|
||
}
|
||
|
||
// Next should be stwu r1, -N(r1)
|
||
let stwu_instr = read_instr(pe, func_addr + prologue_len, image_base).unwrap_or(0);
|
||
if let Some(d) = is_stwu_r1(stwu_instr) {
|
||
frame_size = (-d) as u32;
|
||
prologue_len += 4;
|
||
}
|
||
}
|
||
// Pattern B: stwu r1, -N(r1) without mfspr (rare but possible for leaf-ish functions)
|
||
else if let Some(d) = is_stwu_r1(instr0) {
|
||
frame_size = (-d) as u32;
|
||
prologue_len = 4;
|
||
is_leaf = true; // no LR save = likely leaf (or uses CTR)
|
||
}
|
||
// Pattern C: no prologue — leaf function, just code until blr
|
||
else {
|
||
is_leaf = true;
|
||
}
|
||
|
||
// Walk forward to find the end of the function
|
||
let max_range = code_ranges
|
||
.iter()
|
||
.find(|&&(s, e)| func_addr >= s && func_addr < e)
|
||
.map(|&(_, e)| e)
|
||
.unwrap_or(func_addr + 0x100000);
|
||
|
||
let mut end_addr = func_addr + 4;
|
||
let mut addr = func_addr + prologue_len;
|
||
let scan_limit = std::cmp::min(addr + 0x100000, max_range); // 1MB max function
|
||
|
||
while addr < scan_limit {
|
||
let instr = match read_instr(pe, addr, image_base) {
|
||
Some(i) => i,
|
||
None => break,
|
||
};
|
||
|
||
// Epilogue: blr
|
||
if is_blr(instr) {
|
||
end_addr = addr + 4;
|
||
// Check if the instruction after blr looks like padding or another function
|
||
// Sometimes there's trailing data after blr; we stop at the first blr
|
||
// that isn't inside a branch-over pattern
|
||
break;
|
||
}
|
||
|
||
// Epilogue: b __restgprlr_NN (tail branch into restore stub)
|
||
if let Some(target) = b_target(instr, addr)
|
||
&& let Some(rb) = restore_base
|
||
&& target >= rb
|
||
&& target < rb + 18 * 4
|
||
{
|
||
end_addr = addr + 4;
|
||
break;
|
||
}
|
||
|
||
// Epilogue: bctr (indirect tail call — end of function)
|
||
if is_bctr(instr) {
|
||
end_addr = addr + 4;
|
||
break;
|
||
}
|
||
|
||
addr += 4;
|
||
}
|
||
|
||
// If we didn't find any epilogue within a reasonable range, still emit
|
||
// the function but mark end at the scan point
|
||
if end_addr <= func_addr + 4 && prologue_len > 0 {
|
||
end_addr = addr;
|
||
}
|
||
|
||
// Don't emit zero-size "functions" for addresses that are just data
|
||
if end_addr <= func_addr + 4 && prologue_len == 0 {
|
||
return None;
|
||
}
|
||
|
||
Some(FuncInfo {
|
||
start: func_addr,
|
||
end: end_addr,
|
||
frame_size,
|
||
saved_gprs,
|
||
is_leaf,
|
||
is_saverestore: false,
|
||
pdata_validated: false,
|
||
pdata_length: None,
|
||
pdata_prolog_length: None,
|
||
has_eh: false,
|
||
})
|
||
}
|
||
|
||
// ── Label generation ───────────────────────────────────────────────────────
|
||
|
||
impl FuncAnalysis {
|
||
/// Generate labels for all detected functions.
|
||
/// Call targets with confirmed prologues get `sub_XXXXXXXX`.
|
||
/// Save/restore entries get `__savegprlr_NN` / `__restgprlr_NN`.
|
||
pub fn generate_labels(&self) -> HashMap<u32, String> {
|
||
let mut labels = HashMap::new();
|
||
|
||
for (&addr, fi) in &self.functions {
|
||
if fi.is_saverestore {
|
||
// Label the block start, plus individual register entry points
|
||
if let Some(sb) = self.save_gpr_base
|
||
&& addr == sb
|
||
{
|
||
for i in 0u32..18 {
|
||
let reg = 14 + i;
|
||
labels.insert(sb + i * 4, format!("__savegprlr_{reg}"));
|
||
}
|
||
continue;
|
||
}
|
||
if let Some(rb) = self.restore_gpr_base
|
||
&& addr == rb
|
||
{
|
||
for i in 0u32..18 {
|
||
let reg = 14 + i;
|
||
labels.insert(rb + i * 4, format!("__restgprlr_{reg}"));
|
||
}
|
||
continue;
|
||
}
|
||
}
|
||
labels.insert(addr, format!("sub_{addr:08X}"));
|
||
}
|
||
|
||
labels
|
||
}
|
||
|
||
/// Returns true if `addr` is the start of a detected function.
|
||
pub fn is_function_start(&self, addr: u32) -> bool {
|
||
self.functions.contains_key(&addr)
|
||
}
|
||
|
||
/// Get info for the function starting at `addr`.
|
||
pub fn get(&self, addr: u32) -> Option<&FuncInfo> {
|
||
self.functions.get(&addr)
|
||
}
|
||
}
|