//! M11.5 — static-initialiser driver detection. //! //! MSVC's CRT static-init driver (`_initterm` / `_initterm_e` style) //! is a tight loop that walks a function-pointer array between two //! addresses, calling each non-null entry: //! //! ```text //! loop_top: //! cmpw[l] rA, rB ; compare cursor vs end //! beq done //! lwz rN, 0(rA) ; load fn ptr //! cmpwi rN, 0 ; null-skip (optional) //! beq skip //! mtctr rN //! bcctrl //! skip: //! addi rA, rA, 4 //! b loop_top //! done: //! ``` //! //! Two static addresses (`rA` and `rB` at loop start) bracket the //! function-pointer array. Detection strategy: scan every function for //! the canonical pattern; when found, extract the array bounds and //! emit one row in `function_pointer_arrays` with `kind='static_init'`. //! //! ### What this layer does //! //! - Walks each function looking for an `lwz; mtctr; bcctrl` sequence //! inside a loop bounded by a comparison against another constant. //! - When the loop's cursor register is observed to be incremented by //! exactly 4 per iteration, classifies it as a static-init driver //! and records the (start, end) array bounds. //! //! ### What this layer does NOT do //! //! - No support for back-to-back drivers sharing a common loop trampoline. //! - No detection of the M11 prologue-style heuristic; M11.5 is //! structure-grounded and replaces the prior heuristic where it fires. //! - Does not handle CRT-style `_initterm_e` (the `_e` variant returns //! a status); detection works for both as long as the loop shape //! matches. //! //! Reference: Microsoft CRT `crt0.c::_initterm` source pattern. use std::collections::{BTreeSet, HashMap, HashSet}; use crate::func::FuncAnalysis; use crate::funcptr_arrays::FuncPtrArray; use xenia_xex::pe::PeSection; #[derive(Debug, Clone, Copy)] pub struct StaticInitDriver { /// VA of the driver function (the one containing the loop). pub driver_function: u32, /// VA of the array start. pub array_start: u32, /// VA one-past-end of the array. pub array_end: u32, /// Detected length in slots. pub length: u32, } #[derive(Debug, Default)] pub struct StaticInitResult { pub drivers: Vec, /// Newly-detected static-init arrays, ready to be merged into the /// `function_pointer_arrays` table with `kind='static_init'`. pub arrays: Vec, } const OP_ADDI: u32 = 14; const OP_ADDIS: u32 = 15; const OP_BCCTR: u32 = 19; const OP_LWZ: u32 = 32; const OP_X_FORM: u32 = 31; #[derive(Debug, Clone, Copy)] enum RegVal { Const(u32), } #[tracing::instrument(skip_all, fields(image_base = format_args!("{:#010x}", image_base)))] pub fn analyze( pe: &[u8], image_base: u32, sections: &[PeSection], func_analysis: &FuncAnalysis, function_starts: &BTreeSet, labels: &HashMap, ) -> StaticInitResult { let started = std::time::Instant::now(); let block_boundaries: HashSet = labels.keys().copied().collect(); let mut drivers: Vec = Vec::new(); for (&fn_start, fi) in &func_analysis.functions { if fi.is_saverestore { continue; } if let Some(d) = scan_function_for_driver( pe, image_base, fn_start, fi.end, &block_boundaries, ) { drivers.push(d); } } // Build arrays from the discovered drivers + section data. let mut arrays: Vec = Vec::new(); for d in &drivers { if let Some(entries) = read_array(pe, image_base, sections, d.array_start, d.array_end, function_starts) { arrays.push(FuncPtrArray { address: d.array_start, length: entries.len() as u32, kind: "static_init", entries, }); } } let elapsed_ms = started.elapsed().as_millis() as f64; metrics::histogram!("analysis.phase_ms", "phase" => "static_init").record(elapsed_ms); tracing::info!( drivers = drivers.len(), arrays = arrays.len(), elapsed_ms, "M11.5 static-init driver scan complete", ); StaticInitResult { drivers, arrays } } /// Read the function-pointer array between [start, end) from .rdata/.data. /// NULL entries are skipped (CRT _initterm explicitly tolerates them). /// Non-function-start entries cause us to bail (the driver bounds were /// likely misidentified). fn read_array( pe: &[u8], image_base: u32, sections: &[PeSection], start: u32, end: u32, function_starts: &BTreeSet, ) -> Option> { if end <= start || (end - start) > 4096 { return None; } let _section = sections.iter().find(|s| { let lo = image_base + s.virtual_address; let hi = lo + s.virtual_size; start >= lo && end <= hi && (s.name == ".rdata" || s.name == ".data") })?; let mut entries = Vec::new(); let mut p = start; while p < end { let off = p.wrapping_sub(image_base) as usize; if off + 4 > pe.len() { return None; } let v = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]); if v != 0 { if !function_starts.contains(&v) { return None; } entries.push(v); } p = p.wrapping_add(4); } if entries.is_empty() { return None; } Some(entries) } /// Walk one function looking for the canonical static-init driver shape. /// Returns Some when the loop's cursor register starts at a known constant /// `rA`, terminates at another known constant `rB` via a compare, and /// increments by 4 per iteration with an `lwz; mtctr; bcctrl` body. fn scan_function_for_driver( pe: &[u8], image_base: u32, fn_start: u32, fn_end: u32, block_boundaries: &HashSet, ) -> Option { let mut reg: [Option; 32] = [None; 32]; // Pattern features observed during the walk. let mut cursor_reg: Option = None; let mut cursor_init: Option = None; let mut end_reg: Option = None; let mut end_init: Option = None; let mut saw_lwz_through_cursor = false; let mut saw_mtctr = false; let mut saw_bcctrl = false; let mut saw_addi_4 = false; let mut pc = fn_start; while pc < fn_end { if pc != fn_start && block_boundaries.contains(&pc) { // Heuristic: when we cross a basic-block boundary that // is not the loop-top, accumulated state remains valid for // pattern-matching purposes — but we drop register Const // tracking to be safe. reg = [None; 32]; } let off = pc.wrapping_sub(image_base) as usize; if off + 4 > pe.len() { break; } let instr = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]); let op = instr >> 26; let rd = ((instr >> 21) & 0x1F) as usize; let ra = ((instr >> 16) & 0x1F) as usize; let simm = ((instr & 0xFFFF) as i16) as i32; let uimm = instr & 0xFFFF; match op { OP_ADDIS if ra == 0 => reg[rd] = Some(RegVal::Const(uimm << 16)), OP_ADDIS => { if let Some(RegVal::Const(b)) = reg[ra] { reg[rd] = Some(RegVal::Const(b.wrapping_add(uimm << 16))); } else { reg[rd] = None; } } OP_ADDI if ra != 0 => { let prev = reg[ra]; if let Some(RegVal::Const(b)) = prev { let v = b.wrapping_add(simm as u32); reg[rd] = Some(RegVal::Const(v)); // Was this an `addi r, r, 4`? Mark cursor-increment. if rd == ra && simm == 4 { if Some(rd) == cursor_reg { saw_addi_4 = true; } } else if cursor_reg.is_none() { // First time we see a known-constant register that // *could* be the cursor — defer the choice until we // see a load through it. cursor_init = Some(v); cursor_reg = Some(rd); } else if end_reg.is_none() && Some(rd) != cursor_reg { end_init = Some(v); end_reg = Some(rd); } } else { reg[rd] = None; } } OP_LWZ => { if ra != 0 && Some(ra) == cursor_reg { saw_lwz_through_cursor = true; } reg[rd] = None; } OP_X_FORM => { let xo = (instr >> 1) & 0x3FF; if xo == 467 { let spr = (((instr >> 11) & 0x1F) << 5) | ((instr >> 16) & 0x1F); if spr == 9 && saw_lwz_through_cursor { saw_mtctr = true; } } if xo != 444 && xo != 467 { reg[rd] = None; } } OP_BCCTR => { let xo = (instr >> 1) & 0x3FF; let lk = (instr & 1) != 0; if xo == 528 && lk && saw_mtctr { saw_bcctrl = true; } } 18 => { if (instr & 1) != 0 { for r in 0..=12 { reg[r] = None; } } } 16 => { if (instr & 1) != 0 { for r in 0..=12 { reg[r] = None; } } } _ => {} } pc = pc.wrapping_add(4); } // Validate that all four pattern features fired. if !(saw_lwz_through_cursor && saw_mtctr && saw_bcctrl && saw_addi_4) { return None; } let cursor_init = cursor_init?; let end_init = end_init?; if end_init <= cursor_init { return None; } if end_init - cursor_init > 4096 { return None; } Some(StaticInitDriver { driver_function: fn_start, array_start: cursor_init, array_end: end_init, length: (end_init - cursor_init) / 4, }) } #[cfg(test)] mod tests { use super::*; use crate::func::FuncInfo; use std::collections::BTreeMap; use xenia_xex::pe::PeSection; fn mk_section(name: &str, va: u32, size: u32) -> PeSection { PeSection { name: name.into(), virtual_address: va, virtual_size: size, raw_offset: va, raw_size: size, flags: 0x4000_0040, } } fn write_be(pe: &mut [u8], at: usize, v: u32) { pe[at..at + 4].copy_from_slice(&v.to_be_bytes()); } #[test] fn detects_canonical_initterm_loop() { // Build a tiny driver that loops over a 3-entry array. let image_base = 0x82000000u32; let mut pe = vec![0u8; 0x4000]; // Array at .rdata + 0x800: 3 function pointers. let arr_va_lo = 0x800u32; let fns = [image_base + 0x2000, image_base + 0x2010, image_base + 0x2020]; for (i, p) in fns.iter().enumerate() { write_be(&mut pe, arr_va_lo as usize + i * 4, *p); } let array_start = image_base + arr_va_lo; let array_end = array_start + 12; // Driver function at 0x82001000: // lis r3, hi(array_start) // addi r3, r3, lo(array_start) // lis r4, hi(array_end) // addi r4, r4, lo(array_end) // lwz r5, 0(r3) // mtctr r5 // bcctrl // addi r3, r3, 4 // blr let driver = 0x82001000u32; let off = (driver - image_base) as usize; let lis_r3 = (15u32 << 26) | (3 << 21) | ((array_start >> 16) as u32); let addi_r3 = (14u32 << 26) | (3 << 21) | (3 << 16) | ((array_start as u16) as u32); let lis_r4 = (15u32 << 26) | (4 << 21) | ((array_end >> 16) as u32); let addi_r4 = (14u32 << 26) | (4 << 21) | (4 << 16) | ((array_end as u16) as u32); let lwz = (32u32 << 26) | (5 << 21) | (3 << 16); let mtctr = (31u32 << 26) | (5 << 21) | (9 << 16) | (467 << 1); let bcctrl = (19u32 << 26) | (20 << 21) | (528 << 1) | 1; let addi_inc = (14u32 << 26) | (3 << 21) | (3 << 16) | 4; let blr = (19u32 << 26) | (20 << 21) | (16 << 1); for (i, w) in [lis_r3, addi_r3, lis_r4, addi_r4, lwz, mtctr, bcctrl, addi_inc, blr].iter().enumerate() { write_be(&mut pe, off + i * 4, *w); } let mut functions: BTreeMap = BTreeMap::new(); functions.insert(driver, FuncInfo { start: driver, end: driver + 0x40, frame_size: 0, saved_gprs: 0, is_leaf: false, is_saverestore: false, pdata_validated: false, pdata_length: None, has_eh: false, }); let fa = FuncAnalysis { functions, save_gpr_base: None, restore_gpr_base: None, pdata_entries: Vec::new(), }; let sections = vec![mk_section(".rdata", 0x800, 0x100)]; let mut starts = BTreeSet::new(); for &p in &fns { starts.insert(p); } let labels: HashMap = HashMap::new(); let r = analyze(&pe, image_base, §ions, &fa, &starts, &labels); assert_eq!(r.drivers.len(), 1, "should detect one driver"); let d = &r.drivers[0]; assert_eq!(d.driver_function, driver); assert_eq!(d.array_start, array_start); assert_eq!(d.array_end, array_end); assert_eq!(d.length, 3); assert_eq!(r.arrays.len(), 1); assert_eq!(r.arrays[0].kind, "static_init"); assert_eq!(r.arrays[0].entries.len(), 3); } #[test] fn rejects_function_without_pattern() { let image_base = 0x82000000u32; let mut pe = vec![0u8; 0x4000]; let driver = 0x82001000u32; // Just a blr — no driver pattern. let blr = (19u32 << 26) | (20 << 21) | (16 << 1); write_be(&mut pe, (driver - image_base) as usize, blr); let mut functions: BTreeMap = BTreeMap::new(); functions.insert(driver, FuncInfo { start: driver, end: driver + 0x40, frame_size: 0, saved_gprs: 0, is_leaf: true, is_saverestore: false, pdata_validated: false, pdata_length: None, has_eh: false, }); let fa = FuncAnalysis { functions, save_gpr_base: None, restore_gpr_base: None, pdata_entries: Vec::new(), }; let sections = vec![mk_section(".rdata", 0x800, 0x100)]; let starts: BTreeSet = BTreeSet::new(); let labels: HashMap = HashMap::new(); let r = analyze(&pe, image_base, §ions, &fa, &starts, &labels); assert_eq!(r.drivers.len(), 0); } }