//! The overlay's load base, confirmed against the running game. //! //! The overlay's own profiler table carries absolute RAM addresses pointing //! back at its strings, so the base can be derived from disc bytes (see //! `overlay_load_base_from_the_profiler_table` in the disc tests). This module //! does the part only the emulator can do: read a savestate and check the //! bytes are where the base predicts. //! //! The check matters because the two failures are different. A wrong base puts //! the names somewhere else and finds nothing; a right base that finds nothing //! means the loader relocated the table. Distinguishing those is the whole //! point of confirming rather than deriving. use anyhow::{anyhow, Result}; use gc_formats::overlay::OverlayHeader; use crate::ee::EeRam; /// What the check found. #[derive(Debug, Clone)] pub struct OverlayBaseReport { /// The overlay's payload load base, from its own header. pub load_base: u64, /// Bytes of the overlay compared against RAM. pub overlay_bytes: usize, /// Bytes byte-identical at the predicted addresses. pub verbatim: usize, /// The four profiler names, and whether RAM held each at its predicted /// address. pub names: Vec, } impl OverlayBaseReport { /// Fraction of the overlay the loader left in place. pub fn verbatim_fraction(&self) -> f64 { self.verbatim as f64 / self.overlay_bytes.max(1) as f64 } } /// Check one level's overlay against a RAM image. /// /// `overlay` is the level's `overlay.bin`; `ram` is a 32 MiB EE RAM image taken /// while that level is loaded. pub fn verify(overlay: &[u8], ram: &EeRam) -> Result { // The profiler table's distinctive name run, and the four names before it. let Some(p) = overlay.windows(15).position(|w| w == b"cam+chr\0volume\0") else { return Err(anyhow!( "the overlay carries no profiler name run; is this an overlay?" )); }; let names_at = p - 16; // The base is computed after the table is found, so an all-zero overlay // used as a negative control does not underflow first. let hdr = OverlayHeader::parse(overlay)?; let load_base = hdr.ram_address(0); let mut names = Vec::new(); for k in 0..4usize { let at = hdr.ram_address(names_at + k * 8) as usize; let want = &overlay[names_at + k * 8..names_at + k * 8 + 8]; names.push(ram.slice(at, 8) == Some(want)); } // How much of the overlay did the loader leave in place? The base locates // the image; this measures whether it also locates the tables inside it. let base = load_base as usize; let image = ram.slice(base, overlay.len()).unwrap_or(&[]); let verbatim = overlay .iter() .zip(image.iter()) .filter(|(a, b)| *a == *b) .count(); Ok(OverlayBaseReport { load_base, overlay_bytes: overlay.len(), verbatim, names, }) } /// Render a report for a terminal. pub fn print_report(r: &OverlayBaseReport, out: &mut dyn std::fmt::Write) -> std::fmt::Result { writeln!( out, "overlay load base: {} ({:#x})", r.load_base, r.load_base )?; writeln!( out, " profiler names at the predicted addresses: {}/{}", r.names.iter().filter(|&&b| b).count(), r.names.len() )?; writeln!( out, " overlay bytes byte-identical in RAM: {}/{} ({:.2}%)", r.verbatim, r.overlay_bytes, 100.0 * r.verbatim_fraction() )?; if r.names.iter().all(|&b| b) { writeln!( out, " the base is confirmed: every name is where the overlay's own table says" ) } else { writeln!( out, " the base did NOT place every name where the overlay's table says" ) } } #[cfg(test)] mod tests { use super::*; use crate::ee::EE_SIZE; /// An overlay carrying the profiler table, whose four names sit at /// `names_at` and whose four addresses point back at them. fn overlay() -> Vec { let mut v = vec![0u8; 0x6000]; // Header: field_00 is the payload's load address. v[0..4].copy_from_slice(&1_735_808u32.to_le_bytes()); let names_at = 0x5310usize; for (k, name) in ["normal\0", "camera\0", "cam+chr", "volume\0"] .iter() .enumerate() { let mut slot = [0u8; 8]; slot[..name.len()].copy_from_slice(name.as_bytes()); v[names_at + k * 8..names_at + k * 8 + 8].copy_from_slice(&slot); } // The address run, pointing back at the four names. let addrs_at = names_at + 4 * 8; for k in 0..4usize { let a = 1_735_808u32 - 16 + (names_at + k * 8) as u32; v[addrs_at + k * 4..addrs_at + k * 4 + 4].copy_from_slice(&a.to_le_bytes()); } v } #[test] fn reads_the_base_and_relocates_the_names() { let ov = overlay(); let hdr = OverlayHeader::parse(&ov).unwrap(); assert_eq!(hdr.field_00, 1_735_808); assert_eq!(hdr.ram_address(0), 1_735_792); // The derived base matches the header word less the header's size. let addrs_at = 0x5310 + 32; let a0 = u32::from_le_bytes(ov[addrs_at..addrs_at + 4].try_into().unwrap()); assert_eq!(a0 as u64 - 0x5310, hdr.ram_address(0)); } #[test] fn verifies_against_a_ram_image() { let ov = overlay(); let mut ram = vec![0u8; EE_SIZE]; let base = 1_735_792usize; // The loader places overlay offset 0 at the base. ram[base..base + ov.len()].copy_from_slice(&ov); let r = verify(&ov, &EeRam::new(&ram).unwrap()).unwrap(); assert!(r.names.iter().all(|&b| b), "{:?}", r.names); assert_eq!(r.overlay_bytes, ov.len()); assert_eq!(r.verbatim, ov.len()); } #[test] fn an_overlay_without_the_profiler_table_is_an_error() { // All zeros: no profiler table, and a field_00 of 0 that would // underflow if the base were computed before the table were found. let ov = vec![0u8; 0x6000]; assert!(verify(&ov, &EeRam::new(&vec![0u8; EE_SIZE]).unwrap()).is_err()); } #[test] fn a_wrong_base_finds_nothing_and_says_so() { // An empty RAM: the names are not at the predicted addresses, and only // the fixture's zero bytes line up with the zeros there. let ov = overlay(); let nonzero = ov.iter().filter(|&&b| b != 0).count(); let ram = vec![0u8; EE_SIZE]; let r = verify(&ov, &EeRam::new(&ram).unwrap()).unwrap(); assert_eq!(r.names, vec![false; 4]); assert_eq!(r.verbatim, ov.len() - nonzero); } }