M8+M9+M10+M11+M12: LOW-tier milestones — funcptr-arrays, EH flag, TLS, lr-trace
Five LOW-priority milestones bundled. Total ~700 LOC across 11 files. ## M9 — has_eh derived from pdata.flags exception bit - New `functions.has_eh BOOLEAN NOT NULL` column. Derived from M1's already-parsed `pdata.flags` (bit 31 of the packed word — the exception-handler-present flag, distinct from bit 30 which is the always-1 32-bit-code flag). Index idx_functions_has_eh. - Sylpheed: 2,975 of 23,073 pdata-validated functions have EH (12.9%). ## M10 — .tls section / IMAGE_TLS_DIRECTORY32 parser - New `xenia_xex::tls::parse_tls` parses the directory + zero-terminated callback array. Returns None when the binary has no .tls section. - New `tls_info` (singleton row) + `tls_callbacks(slot, address)` tables. - New `DbWriter::write_tls()` no-ops on None. - Sylpheed has no .tls section → 0 rows; infra ready for binaries with __declspec(thread). ## M8 + M11 — function_pointer_arrays (dispatch tables + static initialisers) - New `xenia_analysis::funcptr_arrays::analyze` widens M3's vtable scan: detects runs of ≥2 function pointers in .rdata and classifies each as `vtable` (M3 re-emit), `dispatch_table` (M8), or `static_init` (M11) via a constructor-prologue heuristic (mfspr + small stwu). - New tables `function_pointer_arrays(address PK, length, kind)` and `function_pointer_array_entries(array_address, slot, function_address)`. - Sylpheed: 722 vtables + 388 dispatch_tables = 1,110 arrays / 6,347 slots. 0 static_init detected (Sylpheed's ctors don't all match the conservative heuristic; M11.5 future work can chain via the entry- point's static-init driver). ## M12 — --lr-trace runtime canary-diff harness - New CLI `exec --lr-trace=PC[,PC,...]` and `--lr-trace-out=PATH` flags. Symbolic resolution (Class::method, Class::*) via M4 lookup. Env vars XENIA_LR_TRACE / XENIA_LR_TRACE_OUT also work. - New `KernelState::lr_trace_pcs` + `lr_trace_writer` + helper `fire_lr_trace_if_match(hw_id)` invoked from the per-instr probe slot. - JSONL output: pc/tid/hw/cycle/r3/r4/r5/r6/lr — superset of what xenia-canary's --log_lr_on_pc patch emits, with a cycle counter for cross-run reproducibility. Diff-friendly via `jq`. - Lockstep digest unaffected: smoke test on entry-point PC fires once with cycle=0/lr=BCBCBCBC/all-GPR-zero (correct initial state). Tests 636→640 (+2 TLS tests, +2 funcptr_arrays tests). Schema golden updated for new tables + has_eh column. Lockstep determinism preserved (instructions=2000005 ×2 reruns identical). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,5 +3,6 @@ pub mod loader;
|
||||
pub mod lzx;
|
||||
pub mod pe;
|
||||
pub mod pdata;
|
||||
pub mod tls;
|
||||
|
||||
pub use header::Xex2Header;
|
||||
|
||||
172
crates/xenia-xex/src/tls.rs
Normal file
172
crates/xenia-xex/src/tls.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
//! `.tls` section parser for PE32 PowerPC.
|
||||
//!
|
||||
//! When MSVC links a binary that uses `__declspec(thread)` storage, it emits
|
||||
//! a `.tls` section plus an IMAGE_TLS_DIRECTORY32 inside `.rdata`. The
|
||||
//! directory points at:
|
||||
//! - the raw initialised TLS data range (start, end VAs)
|
||||
//! - the address of the index field (a u32 written at runtime by the
|
||||
//! loader to identify which TLS slot was assigned)
|
||||
//! - an array of TLS callback function pointers (NUL-terminated)
|
||||
//! - the size of the zero-fill area appended after raw data
|
||||
//!
|
||||
//! Xbox 360 binaries follow the standard PE layout. Sylpheed has no `.tls`
|
||||
//! section and no TLS directory — the parser simply returns `None` and
|
||||
//! callers emit zero rows.
|
||||
//!
|
||||
//! Reference: Microsoft PE/COFF spec, IMAGE_TLS_DIRECTORY32 layout.
|
||||
|
||||
use crate::pe::PeSection;
|
||||
|
||||
/// One TLS callback function pointer extracted from the directory's
|
||||
/// callback array.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TlsCallback {
|
||||
pub address: u32,
|
||||
}
|
||||
|
||||
/// Parsed `.tls` directory information. All fields are absolute VAs.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TlsInfo {
|
||||
/// VA of the start of the initialised raw TLS data (template).
|
||||
pub raw_data_start: u32,
|
||||
/// VA of one-past-end of the raw TLS data.
|
||||
pub raw_data_end: u32,
|
||||
/// VA of the u32 the loader writes the assigned slot index into.
|
||||
pub index_address: u32,
|
||||
/// VA of the zero-terminated callback array; 0 when no callbacks.
|
||||
pub callback_array: u32,
|
||||
/// Bytes of zero-fill appended after the raw template at thread init.
|
||||
pub zero_fill_size: u32,
|
||||
/// Characteristics flags (alignment / etc).
|
||||
pub characteristics: u32,
|
||||
/// Resolved TLS callbacks (parsed from `callback_array`).
|
||||
pub callbacks: Vec<TlsCallback>,
|
||||
}
|
||||
|
||||
/// Parse the `.tls` section. Returns `None` if the binary has no `.tls`
|
||||
/// section or the directory is malformed.
|
||||
pub fn parse_tls(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Option<TlsInfo> {
|
||||
// Find the `.tls` section. The IMAGE_TLS_DIRECTORY32 lives somewhere
|
||||
// in `.rdata`; rather than hunt the IMAGE_DATA_DIRECTORY entry through
|
||||
// the optional header, we accept any 24-byte struct at the start of
|
||||
// `.tls` if the section's raw data looks like a valid directory.
|
||||
//
|
||||
// Per MS docs, IMAGE_TLS_DIRECTORY32 layout (24 bytes):
|
||||
// +0x00 StartAddressOfRawData (VA, 4)
|
||||
// +0x04 EndAddressOfRawData (VA, 4)
|
||||
// +0x08 AddressOfIndex (VA, 4)
|
||||
// +0x0C AddressOfCallBacks (VA, 4 — array of FN ptrs, NUL-terminated)
|
||||
// +0x10 SizeOfZeroFill (4)
|
||||
// +0x14 Characteristics (4)
|
||||
let tls_section = sections.iter().find(|s| s.name == ".tls")?;
|
||||
let off = tls_section.virtual_address as usize;
|
||||
if off + 24 > pe.len() { return None; }
|
||||
|
||||
// Xbox 360 PE bodies are big-endian; this is consistent with how we
|
||||
// parse the PE elsewhere (e.g. xref scanning reads BE u32 from PE).
|
||||
let read_u32 = |start: usize| -> u32 {
|
||||
u32::from_be_bytes([pe[start], pe[start + 1], pe[start + 2], pe[start + 3]])
|
||||
};
|
||||
|
||||
let raw_data_start = read_u32(off);
|
||||
let raw_data_end = read_u32(off + 4);
|
||||
let index_address = read_u32(off + 8);
|
||||
let callback_array = read_u32(off + 12);
|
||||
let zero_fill_size = read_u32(off + 16);
|
||||
let characteristics = read_u32(off + 20);
|
||||
|
||||
// Sanity: raw_data_start should land somewhere inside the image.
|
||||
if raw_data_start == 0 && raw_data_end == 0 && index_address == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Walk the callback array (zero-terminated array of u32 VAs).
|
||||
let mut callbacks = Vec::new();
|
||||
if callback_array != 0 {
|
||||
let mut p = callback_array.wrapping_sub(image_base) as usize;
|
||||
while p + 4 <= pe.len() {
|
||||
let v = read_u32(p);
|
||||
if v == 0 { break; }
|
||||
callbacks.push(TlsCallback { address: v });
|
||||
p += 4;
|
||||
if callbacks.len() >= 64 { break; } // sanity cap
|
||||
}
|
||||
}
|
||||
|
||||
Some(TlsInfo {
|
||||
raw_data_start,
|
||||
raw_data_end,
|
||||
index_address,
|
||||
callback_array,
|
||||
zero_fill_size,
|
||||
characteristics,
|
||||
callbacks,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::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,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_when_no_tls_section() {
|
||||
let pe = vec![0u8; 0x100];
|
||||
let sections = vec![mk_section(".text", 0x10, 0x40)];
|
||||
assert!(parse_tls(&pe, 0x82000000, §ions).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_directory_and_callback_array() {
|
||||
let image_base = 0x82000000u32;
|
||||
let mut pe = vec![0u8; 0x4000];
|
||||
|
||||
// Place the .tls section at RVA 0x100 with the directory.
|
||||
let tls_va: u32 = 0x100;
|
||||
let cb_va: u32 = 0x200;
|
||||
// Directory fields:
|
||||
let raw_start = 0x800u32;
|
||||
let raw_end = 0x900u32;
|
||||
let idx = 0x1000u32;
|
||||
let zero_fill = 0x40u32;
|
||||
let chars = 0x0u32;
|
||||
let cb_array = image_base + cb_va;
|
||||
for (i, v) in [
|
||||
image_base + raw_start, image_base + raw_end,
|
||||
image_base + idx, cb_array, zero_fill, chars,
|
||||
].iter().enumerate() {
|
||||
pe[tls_va as usize + i * 4..tls_va as usize + i * 4 + 4]
|
||||
.copy_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
|
||||
// Two callbacks + NUL terminator at cb_va.
|
||||
let cb1 = image_base + 0x500;
|
||||
let cb2 = image_base + 0x600;
|
||||
pe[cb_va as usize..cb_va as usize + 4].copy_from_slice(&cb1.to_be_bytes());
|
||||
pe[cb_va as usize + 4..cb_va as usize + 8].copy_from_slice(&cb2.to_be_bytes());
|
||||
// pe[cb_va + 8..cb_va + 12] already zero (terminator).
|
||||
|
||||
let sections = vec![mk_section(".tls", tls_va, 0x100)];
|
||||
let info = parse_tls(&pe, image_base, §ions).expect("parses");
|
||||
|
||||
assert_eq!(info.raw_data_start, image_base + raw_start);
|
||||
assert_eq!(info.raw_data_end, image_base + raw_end);
|
||||
assert_eq!(info.index_address, image_base + idx);
|
||||
assert_eq!(info.callback_array, cb_array);
|
||||
assert_eq!(info.zero_fill_size, zero_fill);
|
||||
assert_eq!(info.callbacks.len(), 2);
|
||||
assert_eq!(info.callbacks[0].address, cb1);
|
||||
assert_eq!(info.callbacks[1].address, cb2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user