//! M9.5 — MSVC `__CxxFrameHandler` scope-table parsing. //! //! When MSVC compiles C++ try/catch on Win32 PowerPC, the compiler emits //! per-function `FuncInfo` records in `.rdata` containing the scope-state //! tables that `__CxxFrameHandler` walks during unwinding. Each record //! starts with one of the documented magic numbers: //! //! - `0x19930520` — original FuncInfo (no aligned-state-array) //! - `0x19930521` — adds `pESTypeList` field //! - `0x19930522` — adds `EHFlags` field //! //! Layout (4-byte little-endian on x86; **on Xbox 360 PowerPC PE the //! struct is big-endian** because the binary is BE throughout): //! //! ```text //! +0x00 uint32 magicNumber (one of 0x199305{20,21,22}) //! +0x04 int32 maxState (number of UnwindMapEntry rows) //! +0x08 uint32 pUnwindMap (VA → UnwindMapEntry[]) //! +0x0C uint32 nTryBlocks //! +0x10 uint32 pTryBlockMap (VA → TryBlockMapEntry[]) //! +0x14 uint32 nIPMapEntries (ignored on x86; present on PPC) //! +0x18 uint32 pIPtoStateMap (VA → IPtoStateMapEntry[]) //! +0x1C uint32 pESTypeList (only when magic ≥ 0x19930521) //! +0x20 uint32 EHFlags (only when magic = 0x19930522) //! ``` //! //! Each `UnwindMapEntry` is 8 bytes: `(toState i32, action u32)`. //! Each `TryBlockMapEntry` is 20 bytes: //! `(tryLow i32, tryHigh i32, catchHigh i32, nCatches u32, pHandlerArray u32)`. //! //! ### What this module does //! //! - Magic-scan `.rdata` for the three FuncInfo signatures (read as BE u32). //! - Parse the FuncInfo record + walk the unwind map and try-block map. //! - Skip records whose internal pointers don't land in valid sections, //! or whose lengths exceed sane caps. //! //! ### What this module does NOT do //! //! - Does not associate a FuncInfo back to its owning function. The //! `bl __CxxFrameHandler` registration would name that linkage, but //! it requires walking all `has_eh=true` functions' prologues; a //! future M9.6 can do that. For now the FuncInfo record stands on its //! own — joins to `functions` by best-effort PC range queries. //! - Does not parse the `pHandlerArray` per try-block (catch type info). //! //! Reference: LLVM `llvm/lib/CodeGen/AsmPrinter/WinException.cpp`, //! Microsoft openrce.org documentation on FuncInfo. use xenia_xex::pe::PeSection; const MAGIC_OLD: u32 = 0x1993_0520; const MAGIC_V21: u32 = 0x1993_0521; const MAGIC_V22: u32 = 0x1993_0522; #[derive(Debug, Clone, Copy)] pub struct UnwindMapEntry { pub to_state: i32, pub action_pc: u32, // VA of the cleanup action; 0 if none } #[derive(Debug, Clone, Copy)] pub struct TryBlockMapEntry { pub try_low: i32, pub try_high: i32, pub catch_high: i32, pub n_catches: u32, pub p_handler_array: u32, } #[derive(Debug, Clone)] pub struct EhFuncInfo { pub address: u32, // VA of the FuncInfo record itself pub magic: u32, pub max_state: i32, pub p_unwind_map: u32, pub n_try_blocks: u32, pub p_try_block_map: u32, pub n_ip_map_entries: u32, pub p_ip_to_state_map: u32, pub p_es_type_list: Option, pub eh_flags: Option, pub unwind_map: Vec, pub try_blocks: Vec, } #[tracing::instrument(skip_all, fields(image_base = format_args!("{:#010x}", image_base)))] pub fn analyze( pe: &[u8], image_base: u32, sections: &[PeSection], ) -> Vec { let started = std::time::Instant::now(); let mut out: Vec = Vec::new(); // Compute the union of valid VA ranges across all sections — used to // sanity-check internal pointers in the FuncInfo records. let valid_ranges: Vec<(u32, u32)> = sections.iter() .map(|s| (image_base + s.virtual_address, image_base + s.virtual_address + s.virtual_size)) .collect(); let in_valid = |va: u32| valid_ranges.iter().any(|(lo, hi)| va >= *lo && va < *hi); let read_u32 = |abs: u32| -> Option { let off = abs.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]])) }; let read_i32 = |abs: u32| -> Option { read_u32(abs).map(|u| u as i32) }; for section in sections { if section.name != ".rdata" { continue; } let raw_start = section.virtual_address as usize; let raw_end = (section.virtual_address + section.virtual_size) as usize; if raw_end > pe.len() { continue; } let bytes = &pe[raw_start..raw_end.min(pe.len())]; let va_base = image_base + section.virtual_address; // Walk on 4-byte alignment looking for the magic. let mut i = 0; while i + 4 <= bytes.len() { if !i.is_multiple_of(4) { i += 1; continue; } let m = u32::from_be_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]); if m == MAGIC_OLD || m == MAGIC_V21 || m == MAGIC_V22 { let addr = va_base + i as u32; if let Some(rec) = parse_funcinfo(addr, m, &read_u32, &read_i32, &in_valid) { out.push(rec); } } i += 4; } } let elapsed_ms = started.elapsed().as_millis() as f64; let n_unwind: usize = out.iter().map(|r| r.unwind_map.len()).sum(); let n_try: usize = out.iter().map(|r| r.try_blocks.len()).sum(); metrics::histogram!("analysis.phase_ms", "phase" => "eh_scope").record(elapsed_ms); tracing::info!( records = out.len(), unwind_entries = n_unwind, try_blocks = n_try, elapsed_ms, "M9.5 EH scope-table scan complete", ); out } fn parse_funcinfo( addr: u32, magic: u32, read_u32: &impl Fn(u32) -> Option, read_i32: &impl Fn(u32) -> Option, in_valid: &impl Fn(u32) -> bool, ) -> Option { let max_state = read_i32(addr + 0x04)?; let p_unwind_map = read_u32(addr + 0x08)?; let n_try_blocks = read_u32(addr + 0x0C)?; let p_try_block_map = read_u32(addr + 0x10)?; let n_ip_map_entries = read_u32(addr + 0x14)?; let p_ip_to_state_map = read_u32(addr + 0x18)?; // Sanity caps: real FuncInfo records have max_state ≤ a few thousand, // n_try_blocks ≤ a few hundred. Reject obviously bogus values that // happened to alias the magic. if !(0..=10_000).contains(&max_state) { return None; } if n_try_blocks > 1_000 { return None; } if n_ip_map_entries > 100_000 { return None; } // Pointers must either be NULL or land in a valid section. if p_unwind_map != 0 && !in_valid(p_unwind_map) { return None; } if p_try_block_map != 0 && !in_valid(p_try_block_map) { return None; } if p_ip_to_state_map != 0 && !in_valid(p_ip_to_state_map) { return None; } let (p_es_type_list, eh_flags) = if magic == MAGIC_V21 { (read_u32(addr + 0x1C), None) } else if magic == MAGIC_V22 { (read_u32(addr + 0x1C), read_u32(addr + 0x20)) } else { (None, None) }; // Walk unwind map (8-byte entries). let mut unwind_map: Vec = Vec::with_capacity(max_state as usize); if p_unwind_map != 0 && max_state > 0 { for i in 0..max_state { let p = p_unwind_map.wrapping_add((i * 8) as u32); let to_state = read_i32(p)?; let action_pc = read_u32(p + 4)?; unwind_map.push(UnwindMapEntry { to_state, action_pc }); } } // Walk try-block map (20-byte entries). let mut try_blocks: Vec = Vec::with_capacity(n_try_blocks as usize); if p_try_block_map != 0 && n_try_blocks > 0 { for i in 0..n_try_blocks { let p = p_try_block_map.wrapping_add(i * 20); let try_low = read_i32(p)?; let try_high = read_i32(p + 4)?; let catch_high = read_i32(p + 8)?; let n_catches = read_u32(p + 12)?; let p_handler_a = read_u32(p + 16)?; try_blocks.push(TryBlockMapEntry { try_low, try_high, catch_high, n_catches, p_handler_array: p_handler_a, }); } } Some(EhFuncInfo { address: addr, magic, max_state, p_unwind_map, n_try_blocks, p_try_block_map, n_ip_map_entries, p_ip_to_state_map, p_es_type_list, eh_flags, unwind_map, try_blocks, }) } #[cfg(test)] mod tests { use super::*; 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()); } fn write_be_i32(pe: &mut [u8], at: usize, v: i32) { pe[at..at + 4].copy_from_slice(&v.to_be_bytes()); } #[test] fn parses_minimal_funcinfo_v0() { let image_base = 0x82000000u32; let rdata_va = 0x1000u32; let mut pe = vec![0u8; 0x4000]; // FuncInfo at .rdata + 0x10. let fi_off = (rdata_va + 0x10) as usize; let fi_va = image_base + rdata_va + 0x10; let unwind_off = (rdata_va + 0x80) as usize; let unwind_va = image_base + rdata_va + 0x80; write_be(&mut pe, fi_off, MAGIC_OLD); // magic write_be_i32(&mut pe, fi_off + 4, 2); // maxState write_be(&mut pe, fi_off + 8, unwind_va); // pUnwindMap write_be(&mut pe, fi_off + 12, 0); // nTryBlocks write_be(&mut pe, fi_off + 16, 0); // pTryBlockMap write_be(&mut pe, fi_off + 20, 0); // nIPMapEntries write_be(&mut pe, fi_off + 24, 0); // pIPtoStateMap // Two unwind entries. write_be_i32(&mut pe, unwind_off, -1); // to_state write_be(&mut pe, unwind_off + 4, image_base + 0x500); // action_pc write_be_i32(&mut pe, unwind_off + 8, 0); write_be(&mut pe, unwind_off + 12, image_base + 0x600); let sections = vec![mk_section(".rdata", rdata_va, 0x100)]; let recs = analyze(&pe, image_base, §ions); assert_eq!(recs.len(), 1); let r = &recs[0]; assert_eq!(r.address, fi_va); assert_eq!(r.magic, MAGIC_OLD); assert_eq!(r.max_state, 2); assert_eq!(r.unwind_map.len(), 2); assert_eq!(r.unwind_map[0].to_state, -1); assert_eq!(r.unwind_map[0].action_pc, image_base + 0x500); assert_eq!(r.try_blocks.len(), 0); } #[test] fn rejects_bogus_max_state() { let image_base = 0x82000000u32; let rdata_va = 0x1000u32; let mut pe = vec![0u8; 0x4000]; let fi_off = (rdata_va + 0x10) as usize; write_be(&mut pe, fi_off, MAGIC_OLD); write_be_i32(&mut pe, fi_off + 4, 0xFFFF); // bogus maxState let sections = vec![mk_section(".rdata", rdata_va, 0x100)]; let recs = analyze(&pe, image_base, §ions); assert_eq!(recs.len(), 0); } }