Files
Sylpheed/crates/sylpheed-xexdb/src/func.rs
MechaCat02 c9dd2cb705 fix(xexdb): clear the lint gate on the imported crates
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>
2026-09-13 20:25:44 +02:00

805 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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)
}
}