M4: class-aware probe tokens via M3 vtable+method tables
CLI extension only — no schema change. Adds symbolic resolution for --pc-probe / --branch-probe / --ctor-probe tokens: - `0xADDR` / `2186674160` — numeric (current behavior, no DB load). - `Class::method` — joins classes × methods × demangled_names. - `Class::*` — joins classes × methods (all slots). - `function_name` — falls back to functions.name for free functions / saverestore stubs / labels. New `xenia_analysis::lookup::resolve_probe_token(db_path, token)` opens the DB read-only ONLY when a token is non-numeric, so legacy numeric flows pay no IO. New `--probe-db PATH` flag (or `XENIA_PROBE_DB` env / default `sylpheed.db` next to the .iso) selects the DB. Symbolic resolution happens BEFORE any guest exec, so it cannot affect the lockstep digest. Verified deterministic across two reruns at -n 2M (instructions=2000005 identical). End-to-end smoke test on Sylpheed: `--pc-probe='ANON_Class_6B674251::*'` resolves to all 45 method PCs of that anonymous class (matching the methods-table row count for that vtable). Tests 621→626 (+5 lookup unit tests covering numeric passthrough, symbolic-without-DB error, Class::method resolution, Class::* expansion, and functions.name fallback). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -224,6 +224,12 @@ enum Commands {
|
||||
/// `--dump-section=BASE:LEN:PATH` end-of-run guest memory snapshot.
|
||||
#[arg(long)]
|
||||
dump_section: Option<String>,
|
||||
/// Path to a `sylpheed.db` (M3-populated) for resolving symbolic
|
||||
/// probe tokens like `Class::method` or `Class::*`. Required only
|
||||
/// if any of the `--*-probe` flags contain a non-numeric token.
|
||||
/// Default: `sylpheed.db` next to the .iso file when present.
|
||||
#[arg(long)]
|
||||
probe_db: Option<String>,
|
||||
},
|
||||
/// Browse XISO disc image contents
|
||||
Browse {
|
||||
@@ -384,6 +390,7 @@ fn main() -> Result<()> {
|
||||
branch_probe,
|
||||
mem_watch,
|
||||
dump_section,
|
||||
probe_db,
|
||||
} => cmd_exec(
|
||||
&path,
|
||||
max_instructions,
|
||||
@@ -407,6 +414,7 @@ fn main() -> Result<()> {
|
||||
branch_probe.as_deref(),
|
||||
mem_watch.as_deref(),
|
||||
dump_section.as_deref(),
|
||||
probe_db.as_deref(),
|
||||
),
|
||||
Commands::Browse { path } => cmd_browse(&path),
|
||||
Commands::Info { path } => cmd_info(&path),
|
||||
@@ -443,6 +451,28 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve where to look for `sylpheed.db` when probe tokens are symbolic.
|
||||
/// Precedence:
|
||||
/// 1. explicit `--probe-db` flag (cmd_arg)
|
||||
/// 2. `XENIA_PROBE_DB` env var
|
||||
/// 3. `sylpheed.db` next to the .iso path (if it exists)
|
||||
/// Returns `None` when no DB is available — resolve_probe_token will then
|
||||
/// only succeed for numeric tokens.
|
||||
fn resolve_probe_db_path(cmd_arg: Option<&str>, iso_path: &str) -> Option<std::path::PathBuf> {
|
||||
if let Some(p) = cmd_arg {
|
||||
return Some(std::path::PathBuf::from(p));
|
||||
}
|
||||
if let Ok(p) = std::env::var("XENIA_PROBE_DB") {
|
||||
if !p.is_empty() { return Some(std::path::PathBuf::from(p)); }
|
||||
}
|
||||
let iso = std::path::Path::new(iso_path);
|
||||
if let Some(parent) = iso.parent() {
|
||||
let candidate = parent.join("sylpheed.db");
|
||||
if candidate.exists() { return Some(candidate); }
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Load XEX data from a path. If the path is an ISO, extract default.xex from it.
|
||||
#[instrument(skip_all, fields(path = %path))]
|
||||
fn load_xex_data(path: &str) -> Result<Vec<u8>> {
|
||||
@@ -613,6 +643,7 @@ fn cmd_exec(
|
||||
branch_probe: Option<&str>,
|
||||
mem_watch: Option<&str>,
|
||||
dump_section: Option<&str>,
|
||||
probe_db: Option<&str>,
|
||||
) -> Result<()> {
|
||||
cmd_exec_inner(
|
||||
path,
|
||||
@@ -637,6 +668,7 @@ fn cmd_exec(
|
||||
branch_probe,
|
||||
mem_watch,
|
||||
dump_section,
|
||||
probe_db,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
@@ -680,6 +712,7 @@ fn cmd_check(
|
||||
None, // branch_probe — diagnostic, never wanted on goldens
|
||||
None, // mem_watch — same
|
||||
None, // dump_section — same
|
||||
None, // probe_db — same
|
||||
out,
|
||||
expect,
|
||||
stable_digest,
|
||||
@@ -709,6 +742,7 @@ fn cmd_exec_inner(
|
||||
branch_probe: Option<&str>,
|
||||
mem_watch: Option<&str>,
|
||||
dump_section: Option<&str>,
|
||||
probe_db: Option<&str>,
|
||||
digest_out: Option<&str>,
|
||||
digest_expect: Option<&str>,
|
||||
stable_digest: bool,
|
||||
@@ -958,11 +992,15 @@ fn cmd_exec_inner(
|
||||
}
|
||||
}
|
||||
|
||||
// Diagnostic. Parse `--ctor-probe=0x8217C850,0x...` (or
|
||||
// `XENIA_CTOR_PROBE=...`) into `kernel.ctor_probe_pcs`. The
|
||||
// worker prologue checks this set on every step; on a hit it
|
||||
// prints a single back-chain capture line. Empty set = no
|
||||
// probes = no-op fast path.
|
||||
// Diagnostic. Parse `--ctor-probe=0x8217C850,...` or symbolic forms like
|
||||
// `Class::method`, `Class::*`, `function_name` (or `XENIA_CTOR_PROBE=...`)
|
||||
// into `kernel.ctor_probe_pcs`. Symbolic resolution reads `--probe-db` /
|
||||
// `XENIA_PROBE_DB` (or the `sylpheed.db` next to the .iso when present).
|
||||
// Empty set = no probes = no-op fast path.
|
||||
//
|
||||
// Symbolic resolution happens BEFORE guest exec begins, so it cannot
|
||||
// affect the lockstep digest.
|
||||
let probe_db_path = resolve_probe_db_path(probe_db, path);
|
||||
let ctor_probe_combined: Option<String> = match (
|
||||
ctor_probe,
|
||||
std::env::var("XENIA_CTOR_PROBE").ok().or_else(|| std::env::var("XENIA_PC_PROBE").ok()),
|
||||
@@ -973,7 +1011,7 @@ fn cmd_exec_inner(
|
||||
};
|
||||
if let Some(list) = ctor_probe_combined {
|
||||
for token in list.split(',').map(str::trim).filter(|s| !s.is_empty()) {
|
||||
let (pc_str, consumer): (&str, Option<(u32, u32)>) = match token.split_once('@') {
|
||||
let (pc_part, consumer): (&str, Option<(u32, u32)>) = match token.split_once('@') {
|
||||
None => (token, None),
|
||||
Some((pc_part, rest)) => {
|
||||
let (disp_str, off_str) = rest.split_once(':').ok_or_else(|| {
|
||||
@@ -990,12 +1028,13 @@ fn cmd_exec_inner(
|
||||
(pc_part.trim(), Some((disp, off)))
|
||||
}
|
||||
};
|
||||
let pc = parse_hex_u32(pc_str).map_err(|e| {
|
||||
anyhow::anyhow!("invalid PC in --pc-probe: {token:?}: {e}")
|
||||
})?;
|
||||
kernel.ctor_probe_pcs.insert(pc);
|
||||
if let Some(c) = consumer {
|
||||
kernel.pc_probe_consumers.insert(pc, c);
|
||||
let pcs = xenia_analysis::lookup::resolve_probe_token(probe_db_path.as_deref(), pc_part)
|
||||
.map_err(|e| anyhow::anyhow!("--pc-probe {token:?}: {e}"))?;
|
||||
for pc in pcs {
|
||||
kernel.ctor_probe_pcs.insert(pc);
|
||||
if let Some(c) = consumer {
|
||||
kernel.pc_probe_consumers.insert(pc, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !quiet && !kernel.ctor_probe_pcs.is_empty() {
|
||||
@@ -1023,10 +1062,11 @@ fn cmd_exec_inner(
|
||||
};
|
||||
if let Some(list) = branch_probe_combined {
|
||||
for token in list.split(',').map(str::trim).filter(|s| !s.is_empty()) {
|
||||
let pc = parse_hex_u32(token).map_err(|e| {
|
||||
anyhow::anyhow!("invalid PC in --branch-probe: {token:?}: {e}")
|
||||
})?;
|
||||
kernel.branch_probe_pcs.insert(pc);
|
||||
let pcs = xenia_analysis::lookup::resolve_probe_token(probe_db_path.as_deref(), token)
|
||||
.map_err(|e| anyhow::anyhow!("--branch-probe {token:?}: {e}"))?;
|
||||
for pc in pcs {
|
||||
kernel.branch_probe_pcs.insert(pc);
|
||||
}
|
||||
}
|
||||
if !quiet && !kernel.branch_probe_pcs.is_empty() {
|
||||
let mut pcs: Vec<u32> = kernel.branch_probe_pcs.iter().copied().collect();
|
||||
|
||||
Reference in New Issue
Block a user