diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs index a4dfa7d..56232fb 100644 --- a/crates/xenia-kernel/src/exports.rs +++ b/crates/xenia-kernel/src/exports.rs @@ -46,7 +46,13 @@ pub fn register_exports(state: &mut KernelState) { state.register_export(Xboxkrnl, 0x81, "KeQueryBasePriorityThread", ke_query_base_priority_thread); state.register_export(Xboxkrnl, 0x82, "KeQueryIdealProcessor", ke_query_ideal_processor); state.register_export(Xboxkrnl, 0x83, "KeQueryPerformanceFrequency", ke_query_performance_frequency); - state.register_export(Xboxkrnl, 0x84, "KeQuerySystemTime", ke_query_system_time); + // Canary declares `void KeQuerySystemTime_entry(lpqword_t time_ptr, ...)` + // (xboxkrnl_threading.cc:459); the time is delivered via the OUT + // pointer, not via gpr[3]. Phase A's `kernel.return.return_value` + // must be 0 (canary literal) — not r3 (which for ours is the input + // arg `time_ptr` left untouched). See `register_void_export` doc in + // state.rs. + state.register_void_export(Xboxkrnl, 0x84, "KeQuerySystemTime", ke_query_system_time); state.register_export(Xboxkrnl, 0x85, "KeRaiseIrqlToDpcLevel", stub_return_zero); state.register_export(Xboxkrnl, 0x88, "KeReleaseSemaphore", ke_release_semaphore); state.register_export(Xboxkrnl, 0x89, "KeReleaseSpinLockFromRaisedIrql", ke_release_spinlock_from_raised_irql); diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs index b256fe7..b076ff7 100644 --- a/crates/xenia-kernel/src/state.rs +++ b/crates/xenia-kernel/src/state.rs @@ -50,6 +50,17 @@ pub const HMODULE_XAM: u32 = 0xFFFE_0002; /// Central kernel state tracking all guest OS state. pub struct KernelState { exports: HashMap<(ModuleId, u32), (&'static str, KernelExportFn)>, + /// Phase A: kernel exports whose canary signature is `void` (no + /// dword_result_t / pointer_result_t). For symmetry with canary's + /// `if constexpr (std::is_void::value)` trampoline branch + /// (see `xenia-canary/src/xenia/kernel/util/shim_utils.h`), the + /// Phase A `kernel.return` event for these exports emits + /// `return_value=0` instead of `gpr[3]` (which for void fns is + /// just the input arg pointer left untouched). Without this, + /// e.g. `KeQuerySystemTime` — declared `void` in canary, taking a + /// `lpqword_t time_ptr` — would report ours's r3=time_ptr but + /// canary's literal 0, producing a spurious diff. Cvar-OFF inert. + void_exports: std::collections::HashSet<(ModuleId, u32)>, /// M2.4: bump allocator for kernel handles. `AtomicU32` so concurrent /// HLE calls under M3 can `fetch_add` without a lock. `Relaxed` is /// fine — the allocated value is a fresh ID with no prior payload to @@ -264,6 +275,23 @@ pub struct KernelState { pub dump_addrs: Vec, /// `--dump-section=BASE:LEN:PATH` end-of-run snapshot, page-gated by `is_mapped`. pub dump_section: Option<(u32, u32, std::path::PathBuf)>, + /// Phase B initial-state snapshot — directory under which a + /// `ours/{cpu_state,memory,kernel,vfs,config}.json` + `manifest.json` + /// snapshot is written at the moment immediately before the first + /// guest PPC instruction of the XEX entry_point. `None` (default) = + /// disabled, zero overhead. See + /// `xenia-rs/audit-runs/phase-b-state-equivalence/`. + pub phase_b_snapshot_dir: Option, + /// Phase B: after writing the snapshot, exit the process immediately + /// so re-runs are byte-deterministic. Default false. + pub phase_b_snapshot_and_exit: bool, + /// Phase B: include raw bytes in `memory.json`'s `section_contents`. + /// Default false — per-region SHA-256 is enough for the routine diff. + pub phase_b_dump_section_content: bool, + /// Phase B: the XEX entry_point address — captured by the app at + /// `install_initial_thread` time and consulted by the snapshot hook + /// to validate the firing thread is the entry thread. + pub entry_pc: u32, } impl KernelState { @@ -288,6 +316,7 @@ impl KernelState { scheduler.set_reservation_table(Some(reservations.clone())); let mut state = Self { exports: HashMap::new(), + void_exports: std::collections::HashSet::new(), next_handle: AtomicU32::new(0x1000), scheduler, next_tls_index: AtomicU32::new(0), @@ -331,6 +360,10 @@ impl KernelState { lr_trace_writer: None, dump_addrs: Vec::new(), dump_section: None, + phase_b_snapshot_dir: None, + phase_b_snapshot_and_exit: false, + phase_b_dump_section_content: false, + entry_pc: 0, }; crate::exports::register_exports(&mut state); crate::xam::register_exports(&mut state); @@ -377,6 +410,22 @@ impl KernelState { self.exports.insert((module, ordinal), (name, func)); } + /// Register a kernel export whose canary signature is `void`. + /// See `KernelState::void_exports` doc. Identical semantics to + /// `register_export` except the Phase A `kernel.return` payload's + /// `return_value` field is emitted as 0 instead of `gpr[3]`, + /// matching canary's `EmitReturn(name, 0)` branch. + pub fn register_void_export( + &mut self, + module: ModuleId, + ordinal: u32, + name: &'static str, + func: KernelExportFn, + ) { + self.exports.insert((module, ordinal), (name, func)); + self.void_exports.insert((module, ordinal)); + } + /// AUDIT-038 — install a host directory as the backing store for the /// `cache:` mount. The directory is unconditionally cleared (and then /// re-created) on entry so two consecutive runs see byte-identical @@ -514,7 +563,49 @@ impl KernelState { metrics::counter!("kernel.calls", "name" => name).increment(1); tracing::trace!(target: "probe_calls", "hw={} call={} r3={:#x} r4={:#x} r5={:#x} lr={:#x}", r.hw_id, name, ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.lr); + // Phase A event log — see crates/xenia-kernel/src/event_log.rs. + // Hot path: `is_enabled` is a relaxed atomic-bool load. + let phase_a_on = crate::event_log::is_enabled(); + let (phase_a_tid, phase_a_cycle) = if phase_a_on { + let tid = self.scheduler.thread(r).tid; + let cycle = ctx.cycle_count; + (tid, cycle) + } else { + (0u32, 0u64) + }; + if phase_a_on { + let module_name = match module { + ModuleId::Xboxkrnl => "xboxkrnl.exe", + ModuleId::Xam => "xam.xex", + ModuleId::Xbdm => "xbdm.xex", + }; + crate::event_log::emit_import_call( + phase_a_tid, + phase_a_cycle, + module_name, + ordinal as u16, + name, + ); + crate::event_log::emit_kernel_call(phase_a_tid, phase_a_cycle, name); + } + let is_void = self.void_exports.contains(&(module, ordinal)); func(&mut ctx, mem, self); + if phase_a_on { + // Mirror canary's `if constexpr (std::is_void::value)` + // trampoline branch: void exports emit literal 0; non-void + // emit post-call gpr[3]. Without this, void exports that + // take a pointer arg (e.g. `KeQuerySystemTime`) would + // report ours=r3=arg_ptr vs canary=0 — a Phase A diff + // that is purely an emitter-framing asymmetry, not an + // engine semantic divergence. + let return_value = if is_void { 0 } else { ctx.gpr[3] }; + crate::event_log::emit_kernel_return( + phase_a_tid, + ctx.cycle_count, + name, + return_value, + ); + } true } else { metrics::counter!("kernel.unimplemented").increment(1);