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:
MechaCat02
2026-05-08 22:29:35 +02:00
parent 85d1603124
commit 5af792c9fc
11 changed files with 852 additions and 16 deletions

View File

@@ -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
View 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, &sections).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, &sections).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);
}
}