diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs index 0bc4922..409d870 100644 --- a/crates/xenia-app/src/main.rs +++ b/crates/xenia-app/src/main.rs @@ -271,6 +271,15 @@ enum Commands { /// `--audit-mem-read-hex=828E1F08`. #[arg(long)] audit_mem_read_hex: Option, + /// AUDIT-052 — number of bytes (4-byte aligned, max 256) to + /// dump from `r3` on every `--audit-pc-probe-hex` fire. Emits a + /// paired `AUDIT-R3-DUMP` line with the u32 lanes. Designed for + /// the 80-byte stack-local struct at `sub_82452DC0` (`r31+96`) + /// when probing `sub_8245B000` entry — where `r3` IS the struct + /// pointer. Read-only; lockstep digest unaffected. Settable via + /// `XENIA_AUDIT_R3_DUMP_BYTES`. Example: `--audit-r3-dump-bytes=80`. + #[arg(long)] + audit_r3_dump_bytes: Option, }, /// Browse XISO disc image contents Browse { @@ -436,6 +445,7 @@ fn main() -> Result<()> { lr_trace_out, audit_pc_probe_hex, audit_mem_read_hex, + audit_r3_dump_bytes, } => cmd_exec( &path, max_instructions, @@ -464,6 +474,7 @@ fn main() -> Result<()> { lr_trace_out.as_deref(), audit_pc_probe_hex.as_deref(), audit_mem_read_hex.as_deref(), + audit_r3_dump_bytes, ), Commands::Browse { path } => cmd_browse(&path), Commands::Info { path } => cmd_info(&path), @@ -697,6 +708,7 @@ fn cmd_exec( lr_trace_out: Option<&str>, audit_pc_probe_hex: Option<&str>, audit_mem_read_hex: Option<&str>, + audit_r3_dump_bytes: Option, ) -> Result<()> { cmd_exec_inner( path, @@ -726,6 +738,7 @@ fn cmd_exec( lr_trace_out, audit_pc_probe_hex, audit_mem_read_hex, + audit_r3_dump_bytes, None, None, false, @@ -774,6 +787,7 @@ fn cmd_check( None, // lr_trace_out — same None, // audit_pc_probe_hex — diagnostic, never wanted on goldens None, // audit_mem_read_hex — same + None, // audit_r3_dump_bytes — same out, expect, stable_digest, @@ -808,6 +822,7 @@ fn cmd_exec_inner( lr_trace_out: Option<&str>, audit_pc_probe_hex: Option<&str>, audit_mem_read_hex: Option<&str>, + audit_r3_dump_bytes: Option, digest_out: Option<&str>, digest_expect: Option<&str>, stable_digest: bool, @@ -1263,6 +1278,29 @@ fn cmd_exec_inner( } } + // AUDIT-052 — `--audit-r3-dump-bytes=80`. When set, every + // `--audit-pc-probe-hex` fire emits a paired `AUDIT-R3-DUMP` line + // with N bytes from `r3` (4-byte aligned, capped at 256). Sized for + // the 80-byte stack-local struct at `sub_82452DC0`'s `r31+96` — + // probe `sub_8245B000` entry where `r3 == parent's r31+96`. + let audit_r3_dump_combined: Option = match ( + audit_r3_dump_bytes, std::env::var("XENIA_AUDIT_R3_DUMP_BYTES").ok(), + ) { + (Some(n), _) => Some(n), + (None, Some(s)) if !s.is_empty() => Some( + s.parse::().map_err(|e| anyhow::anyhow!("--audit-r3-dump-bytes {s:?}: {e}"))?, + ), + _ => None, + }; + if let Some(n) = audit_r3_dump_combined { + if n > 0 { + kernel.audit_r3_dump_bytes = Some(n); + if !quiet { + tracing::info!("audit-r3-dump armed: {} bytes", n); + } + } + } + // Diagnostic. Parse `--dump-addr=0x828F3D08,...` (or // `XENIA_DUMP_ADDR=...`) into `kernel.dump_addrs`. The contents // are dumped at end-of-run by `dump_thread_diagnostic`. Pure diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs index b0cea13..cc705d1 100644 --- a/crates/xenia-kernel/src/state.rs +++ b/crates/xenia-kernel/src/state.rs @@ -270,6 +270,15 @@ pub struct KernelState { /// unaffected. Settable via `--audit-mem-read-hex` / /// `XENIA_AUDIT_MEM_READ`. pub audit_mem_read_addr: Option, + /// AUDIT-052 — diagnostic. When set, each `AUDIT-PC-PROBE` fire + /// additionally emits an `AUDIT-R3-DUMP` line with N bytes of guest + /// memory dumped from `r3` as `u32` lanes (4-byte aligned only). + /// Sized for audit-051's 80-byte stack-local struct at `r31+96` + /// inside `sub_82452DC0` (probe `sub_8245B000` entry where + /// `r3 == parent's r31+96`). Read-only; lockstep digest unaffected. + /// Settable via `--audit-r3-dump-bytes` / + /// `XENIA_AUDIT_R3_DUMP_BYTES`. + pub audit_r3_dump_bytes: Option, /// M12 — diagnostic. PCs at which to emit a structured JSONL record /// per fire, designed for diffing against xenia-canary's /// `--log_lr_on_pc` patch output. Each line carries @@ -355,6 +364,7 @@ impl KernelState { branch_probe_pcs: std::collections::HashSet::new(), audit_pc_probe_pcs: std::collections::HashSet::new(), audit_mem_read_addr: None, + audit_r3_dump_bytes: None, lr_trace_pcs: std::collections::HashSet::new(), lr_trace_writer: None, dump_addrs: Vec::new(), @@ -886,6 +896,28 @@ impl KernelState { addr, val, vt, m0, m6, pc, tid, cycle, ); } + // AUDIT-052 — dump N bytes of guest memory from r3 as u32 lanes + // when `audit_r3_dump_bytes` is set. Sized for the 80-byte + // stack-local struct at sub_82452DC0's `r31+96` (probe is + // sub_8245B000 entry where r3 IS the struct ptr). Output + // format: `AUDIT-R3-DUMP pc=… r3=… +0x00=… +0x04=… …`. + if let Some(n) = self.audit_r3_dump_bytes { + let n = n.min(256) & !3u32; // cap 256B, 4-byte align + let mut out = String::with_capacity(64 + (n as usize) * 16); + use std::fmt::Write as _; + let _ = write!( + &mut out, + "AUDIT-R3-DUMP pc={:#010x} tid={} cycle={} r3={:#010x}", + pc, tid, cycle, r3, + ); + let mut off: u32 = 0; + while off < n { + let v = mem.read_u32(r3.wrapping_add(off)); + let _ = write!(&mut out, " +0x{:02x}={:#010x}", off, v); + off = off.wrapping_add(4); + } + println!("{}", out); + } } /// M12 — diagnostic. If the live PC for HW slot `hw_id` is in