feat(memory): --mem-watch=ADDR per-store writer trace
Adds an opt-in diagnostic that emits one tracing line per guest store overlapping any armed byte address, naming the writer (tid, pc, lr) plus old/new u32 lanes. Mirrors the --pc-probe / --branch-probe shape; pc/lr are stamped from worker_prologue via a thread-local Cell, so default runs (empty watch set) take a single is_empty() check on each write. Lockstep digest preserved (instructions=100000003 across reruns, sylpheed_n50m.json golden byte-identical). Diagnostic infra only; no functional change. Used to identify producers of dispatch-state writes for the audit-017 / audit-019 hunt.
This commit is contained in:
@@ -213,6 +213,14 @@ enum Commands {
|
||||
/// `XENIA_BRANCH_PROBE`.
|
||||
#[arg(long)]
|
||||
branch_probe: Option<String>,
|
||||
/// Diagnostic. Comma-separated guest byte addresses; on every
|
||||
/// guest store that overlaps any listed byte, emit one
|
||||
/// `MEM-WATCH` line at tracing target `mem_watch` with the
|
||||
/// (tid, pc, lr) of the writer plus old/new u32 lanes.
|
||||
/// Read-only; lockstep digest unaffected. Settable via
|
||||
/// `XENIA_MEM_WATCH`. Example: `--mem-watch=0x828F40B4`.
|
||||
#[arg(long)]
|
||||
mem_watch: Option<String>,
|
||||
},
|
||||
/// Browse XISO disc image contents
|
||||
Browse {
|
||||
@@ -371,6 +379,7 @@ fn main() -> Result<()> {
|
||||
ctor_probe,
|
||||
dump_addr,
|
||||
branch_probe,
|
||||
mem_watch,
|
||||
} => cmd_exec(
|
||||
&path,
|
||||
max_instructions,
|
||||
@@ -392,6 +401,7 @@ fn main() -> Result<()> {
|
||||
ctor_probe.as_deref(),
|
||||
dump_addr.as_deref(),
|
||||
branch_probe.as_deref(),
|
||||
mem_watch.as_deref(),
|
||||
),
|
||||
Commands::Browse { path } => cmd_browse(&path),
|
||||
Commands::Info { path } => cmd_info(&path),
|
||||
@@ -596,6 +606,7 @@ fn cmd_exec(
|
||||
ctor_probe: Option<&str>,
|
||||
dump_addr: Option<&str>,
|
||||
branch_probe: Option<&str>,
|
||||
mem_watch: Option<&str>,
|
||||
) -> Result<()> {
|
||||
cmd_exec_inner(
|
||||
path,
|
||||
@@ -618,6 +629,7 @@ fn cmd_exec(
|
||||
ctor_probe,
|
||||
dump_addr,
|
||||
branch_probe,
|
||||
mem_watch,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
@@ -659,6 +671,7 @@ fn cmd_check(
|
||||
None, // ctor_probe — diagnostic, never wanted on goldens
|
||||
None, // dump_addr — same
|
||||
None, // branch_probe — diagnostic, never wanted on goldens
|
||||
None, // mem_watch — same
|
||||
out,
|
||||
expect,
|
||||
stable_digest,
|
||||
@@ -686,6 +699,7 @@ fn cmd_exec_inner(
|
||||
ctor_probe: Option<&str>,
|
||||
dump_addr: Option<&str>,
|
||||
branch_probe: Option<&str>,
|
||||
mem_watch: Option<&str>,
|
||||
digest_out: Option<&str>,
|
||||
digest_expect: Option<&str>,
|
||||
stable_digest: bool,
|
||||
@@ -1058,6 +1072,41 @@ fn cmd_exec_inner(
|
||||
}
|
||||
}
|
||||
|
||||
let mem_watch_combined: Option<String> = match (mem_watch, std::env::var("XENIA_MEM_WATCH").ok()) {
|
||||
(Some(s), _) => Some(s.to_string()),
|
||||
(None, Some(s)) if !s.is_empty() => Some(s),
|
||||
_ => None,
|
||||
};
|
||||
let mut mem_watch_addrs: Vec<u32> = Vec::new();
|
||||
if let Some(list) = mem_watch_combined {
|
||||
for token in list.split(',').map(str::trim).filter(|s| !s.is_empty()) {
|
||||
let parsed = if let Some(hex) = token.strip_prefix("0x").or_else(|| token.strip_prefix("0X")) {
|
||||
u32::from_str_radix(hex, 16)
|
||||
} else {
|
||||
token.parse::<u32>()
|
||||
};
|
||||
match parsed {
|
||||
Ok(addr) => mem_watch_addrs.push(addr),
|
||||
Err(_) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"invalid address in --mem-watch: {token:?}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !quiet && !mem_watch_addrs.is_empty() {
|
||||
let strs: Vec<String> = mem_watch_addrs
|
||||
.iter()
|
||||
.map(|a| format!("{a:#010x}"))
|
||||
.collect();
|
||||
tracing::info!(
|
||||
"mem-watch armed: {} ({})",
|
||||
mem_watch_addrs.len(),
|
||||
strs.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Install the GPU register aperture MMIO region on the guest memory so
|
||||
// any `0x7FC8xxxx` access routes to our atomic mailbox. Matches canary's
|
||||
// `graphics_system.cc:141-144`. The callbacks capture Arc clones of the
|
||||
@@ -1272,6 +1321,10 @@ fn cmd_exec_inner(
|
||||
|
||||
// M1.4 — wrap `mem` in an `Arc<GuestMemory>` after all init mutations
|
||||
// are complete. The worker thread (if spawned below) holds its own
|
||||
if !mem_watch_addrs.is_empty() {
|
||||
mem.arm_mem_watch(mem_watch_addrs);
|
||||
}
|
||||
|
||||
// Arc clone for the duration of the run; the CPU side passes
|
||||
// `&*mem_arc` (= `&GuestMemory`) into `run_execution`. The
|
||||
// trait-level invariant carrying this is correctness: writes are
|
||||
@@ -1999,6 +2052,12 @@ fn worker_prologue(
|
||||
kernel.fire_ctor_probe_if_match(hw_id, mem);
|
||||
kernel.fire_branch_probe_if_match(hw_id);
|
||||
|
||||
if mem.has_mem_watch() {
|
||||
let ctx = kernel.scheduler.ctx(hw_id);
|
||||
let tid = kernel.scheduler.tid(hw_id).unwrap_or(0);
|
||||
xenia_memory::set_writer_ctx(tid, ctx.pc, ctx.lr as u32);
|
||||
}
|
||||
|
||||
// 1) Halt-sentinel check (per HW thread).
|
||||
if pc == LR_HALT {
|
||||
let injected_here = kernel.interrupts.saved.is_some()
|
||||
|
||||
Reference in New Issue
Block a user