add --dump-section=BASE:LEN:PATH for end-of-run guest memory snapshot
Drives byte-level memory diffs against canary's Memory::Save dump. Hot-path zero-cost when absent; lockstep digest unaffected (instructions=100000003 deterministic across reruns).
This commit is contained in:
@@ -221,6 +221,13 @@ enum Commands {
|
|||||||
/// `XENIA_MEM_WATCH`. Example: `--mem-watch=0x828F40B4`.
|
/// `XENIA_MEM_WATCH`. Example: `--mem-watch=0x828F40B4`.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
mem_watch: Option<String>,
|
mem_watch: Option<String>,
|
||||||
|
/// Diagnostic. Dump a contiguous guest memory range to a file at
|
||||||
|
/// end-of-run. Format: `BASE:LEN:PATH` (BASE/LEN hex or decimal).
|
||||||
|
/// Example: `--dump-section=0x80000000:0x10000000:v80.bin`.
|
||||||
|
/// Read via `GuestMemory::read_bulk`; uncommitted pages read as
|
||||||
|
/// zero. Read-only; lockstep digest unaffected.
|
||||||
|
#[arg(long)]
|
||||||
|
dump_section: Option<String>,
|
||||||
},
|
},
|
||||||
/// Browse XISO disc image contents
|
/// Browse XISO disc image contents
|
||||||
Browse {
|
Browse {
|
||||||
@@ -380,6 +387,7 @@ fn main() -> Result<()> {
|
|||||||
dump_addr,
|
dump_addr,
|
||||||
branch_probe,
|
branch_probe,
|
||||||
mem_watch,
|
mem_watch,
|
||||||
|
dump_section,
|
||||||
} => cmd_exec(
|
} => cmd_exec(
|
||||||
&path,
|
&path,
|
||||||
max_instructions,
|
max_instructions,
|
||||||
@@ -402,6 +410,7 @@ fn main() -> Result<()> {
|
|||||||
dump_addr.as_deref(),
|
dump_addr.as_deref(),
|
||||||
branch_probe.as_deref(),
|
branch_probe.as_deref(),
|
||||||
mem_watch.as_deref(),
|
mem_watch.as_deref(),
|
||||||
|
dump_section.as_deref(),
|
||||||
),
|
),
|
||||||
Commands::Browse { path } => cmd_browse(&path),
|
Commands::Browse { path } => cmd_browse(&path),
|
||||||
Commands::Info { path } => cmd_info(&path),
|
Commands::Info { path } => cmd_info(&path),
|
||||||
@@ -607,6 +616,7 @@ fn cmd_exec(
|
|||||||
dump_addr: Option<&str>,
|
dump_addr: Option<&str>,
|
||||||
branch_probe: Option<&str>,
|
branch_probe: Option<&str>,
|
||||||
mem_watch: Option<&str>,
|
mem_watch: Option<&str>,
|
||||||
|
dump_section: Option<&str>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
cmd_exec_inner(
|
cmd_exec_inner(
|
||||||
path,
|
path,
|
||||||
@@ -630,6 +640,7 @@ fn cmd_exec(
|
|||||||
dump_addr,
|
dump_addr,
|
||||||
branch_probe,
|
branch_probe,
|
||||||
mem_watch,
|
mem_watch,
|
||||||
|
dump_section,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
@@ -672,6 +683,7 @@ fn cmd_check(
|
|||||||
None, // dump_addr — same
|
None, // dump_addr — same
|
||||||
None, // branch_probe — diagnostic, never wanted on goldens
|
None, // branch_probe — diagnostic, never wanted on goldens
|
||||||
None, // mem_watch — same
|
None, // mem_watch — same
|
||||||
|
None, // dump_section — same
|
||||||
out,
|
out,
|
||||||
expect,
|
expect,
|
||||||
stable_digest,
|
stable_digest,
|
||||||
@@ -700,6 +712,7 @@ fn cmd_exec_inner(
|
|||||||
dump_addr: Option<&str>,
|
dump_addr: Option<&str>,
|
||||||
branch_probe: Option<&str>,
|
branch_probe: Option<&str>,
|
||||||
mem_watch: Option<&str>,
|
mem_watch: Option<&str>,
|
||||||
|
dump_section: Option<&str>,
|
||||||
digest_out: Option<&str>,
|
digest_out: Option<&str>,
|
||||||
digest_expect: Option<&str>,
|
digest_expect: Option<&str>,
|
||||||
stable_digest: bool,
|
stable_digest: bool,
|
||||||
@@ -1107,6 +1120,37 @@ fn cmd_exec_inner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dump_section_combined: Option<String> = match (dump_section, std::env::var("XENIA_DUMP_SECTION").ok()) {
|
||||||
|
(Some(s), _) => Some(s.to_string()),
|
||||||
|
(None, Some(s)) if !s.is_empty() => Some(s),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(spec) = dump_section_combined {
|
||||||
|
let parts: Vec<&str> = spec.splitn(3, ':').collect();
|
||||||
|
if parts.len() != 3 {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"--dump-section expects BASE:LEN:PATH, got {spec:?}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let parse_u32 = |t: &str| -> Result<u32> {
|
||||||
|
if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
|
||||||
|
u32::from_str_radix(h, 16).map_err(|e| anyhow::anyhow!("bad hex {t:?}: {e}"))
|
||||||
|
} else {
|
||||||
|
t.parse::<u32>().map_err(|e| anyhow::anyhow!("bad dec {t:?}: {e}"))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let base = parse_u32(parts[0].trim())?;
|
||||||
|
let len = parse_u32(parts[1].trim())?;
|
||||||
|
let path = std::path::PathBuf::from(parts[2].trim());
|
||||||
|
if !quiet {
|
||||||
|
tracing::info!(
|
||||||
|
"dump-section armed: base={:#010x} len={:#x} path={}",
|
||||||
|
base, len, path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
kernel.dump_section = Some((base, len, path));
|
||||||
|
}
|
||||||
|
|
||||||
// Install the GPU register aperture MMIO region on the guest memory so
|
// Install the GPU register aperture MMIO region on the guest memory so
|
||||||
// any `0x7FC8xxxx` access routes to our atomic mailbox. Matches canary's
|
// any `0x7FC8xxxx` access routes to our atomic mailbox. Matches canary's
|
||||||
// `graphics_system.cc:141-144`. The callbacks capture Arc clones of the
|
// `graphics_system.cc:141-144`. The callbacks capture Arc clones of the
|
||||||
@@ -3707,6 +3751,27 @@ fn dump_thread_diagnostic(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some((base, len, ref path)) = kernel.dump_section {
|
||||||
|
let mut buf = vec![0u8; len as usize];
|
||||||
|
const CHUNK: usize = 4096;
|
||||||
|
let mut off = 0usize;
|
||||||
|
while off < buf.len() {
|
||||||
|
let take = CHUNK.min(buf.len() - off);
|
||||||
|
mem.read_bulk(base.wrapping_add(off as u32), &mut buf[off..off + take]);
|
||||||
|
off += take;
|
||||||
|
}
|
||||||
|
match std::fs::write(path, &buf) {
|
||||||
|
Ok(()) => println!(
|
||||||
|
"\n=== dump-section: wrote {} bytes from {:#010x} to {} ===",
|
||||||
|
buf.len(), base, path.display(),
|
||||||
|
),
|
||||||
|
Err(e) => eprintln!(
|
||||||
|
"dump-section: failed to write {}: {e}",
|
||||||
|
path.display(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
|||||||
@@ -237,6 +237,11 @@ pub struct KernelState {
|
|||||||
/// dump is performed by `dump_thread_diagnostic`, never during
|
/// dump is performed by `dump_thread_diagnostic`, never during
|
||||||
/// the hot interpreter loop, so lockstep determinism is unaffected.
|
/// the hot interpreter loop, so lockstep determinism is unaffected.
|
||||||
pub dump_addrs: Vec<u32>,
|
pub dump_addrs: Vec<u32>,
|
||||||
|
/// Diagnostic. Optional contiguous memory range to dump verbatim to a
|
||||||
|
/// file at end-of-run: `(base, length, path)`. Populated from
|
||||||
|
/// `--dump-section=BASE:LEN:PATH`. Bulk read via `GuestMemory::read_bulk`;
|
||||||
|
/// host pages that were never committed read as zero (mmap-reserved).
|
||||||
|
pub dump_section: Option<(u32, u32, std::path::PathBuf)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KernelState {
|
impl KernelState {
|
||||||
@@ -296,6 +301,7 @@ impl KernelState {
|
|||||||
pc_probe_consumers: HashMap::new(),
|
pc_probe_consumers: HashMap::new(),
|
||||||
branch_probe_pcs: std::collections::HashSet::new(),
|
branch_probe_pcs: std::collections::HashSet::new(),
|
||||||
dump_addrs: Vec::new(),
|
dump_addrs: Vec::new(),
|
||||||
|
dump_section: None,
|
||||||
};
|
};
|
||||||
crate::exports::register_exports(&mut state);
|
crate::exports::register_exports(&mut state);
|
||||||
crate::xam::register_exports(&mut state);
|
crate::xam::register_exports(&mut state);
|
||||||
@@ -1412,6 +1418,23 @@ mod tests {
|
|||||||
assert!(state.ctor_probe_pcs.contains(&0x8217_C850));
|
assert!(state.ctor_probe_pcs.contains(&0x8217_C850));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dump_section_field_round_trip_via_read_bulk() {
|
||||||
|
use xenia_memory::page_table::MemoryProtect;
|
||||||
|
let mem = GuestMemory::new().expect("memory init");
|
||||||
|
mem.alloc(0x4000_0000, 0x1000, MemoryProtect::READ | MemoryProtect::WRITE)
|
||||||
|
.expect("page");
|
||||||
|
let probe = [0xDEu8, 0xAD, 0xBE, 0xEF, 0x12, 0x34, 0x56, 0x78];
|
||||||
|
mem.write_bulk(0x4000_0040, &probe);
|
||||||
|
let mut state = KernelState::new();
|
||||||
|
let tmp = std::env::temp_dir().join("xenia_dump_section_round_trip.bin");
|
||||||
|
state.dump_section = Some((0x4000_0040, 8, tmp.clone()));
|
||||||
|
let (base, len, ref _path) = state.dump_section.as_ref().expect("set").clone();
|
||||||
|
let mut out = vec![0u8; len as usize];
|
||||||
|
mem.read_bulk(base, &mut out);
|
||||||
|
assert_eq!(out, probe);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read_ascii_cstring_handles_termination_and_garbage() {
|
fn read_ascii_cstring_handles_termination_and_garbage() {
|
||||||
use xenia_memory::page_table::MemoryProtect;
|
use xenia_memory::page_table::MemoryProtect;
|
||||||
|
|||||||
Reference in New Issue
Block a user