gameboy-rust/crates/gba-core/tests/bios/readback.rs
0xrsydn efdc288db6 fix(bios): preserve documented protected-read words at firmware exits
Place skipped compatibility data at boot, SoftReset, SWI, and IRQ exit PC+8 locations. Preserve the real IRQ callback return instruction as its readback word. Keep CPU and memory behavior image-derived, including caller-supplied images.

Add original boundary, load-lane, caller-state, wait, supplied-image, and builder-fixup regressions. Document the historical failures, research decision, passing public BIOS checkpoint, and remaining fetch-history limits.

Validated on Darwin arm64: 868 workspace tests in debug and release; 799 core/demo tests separately in both profiles; 39 preparation tests; native ROM window tests in both profiles; ten graphics/window smoke modes; fmt, clippy, and rustdoc. Hash-verified public ARM, Thumb, memory, and BIOS suites pass unchanged with identical debug/release reports. BIOS: r12=0, 17606 steps, 197456 nominal cycles, one IRQ entry.
2026-10-06 00:45:28 +07:00

253 lines
9.6 KiB
Rust

//! Firmware boundary outputs, checked through CPU loads rather than host reads.
use super::*;
use gba_core::cpu::Cpu;
const PROBE: u32 = 0x0203_0000;
// A separate CPU leaves the caller's registers and device clocks unchanged.
// All probe instructions run outside BIOS, so they cannot replace its retained word.
fn probe(bus: &mut Memory, thumb: bool, address: u32, arm: u32, half: u16) -> u32 {
let code = if thumb {
vec![
0xe28f_3001, // ADD r3,pc,#1
0xe12f_ff13, // BX r3
u32::from(half) << 16 | 0x4801, // LDR r0,[pc,#4]; load r1
0xe7fe_e7fe,
address,
]
} else {
vec![0xe59f_0004, arm, 0xeaff_fffe, address]
};
for (index, word) in code.into_iter().enumerate() {
bus.write32(PROBE + index as u32 * 4, word).unwrap();
}
let mut cpu = Cpu::new(PROBE);
for _ in 0..if thumb { 4 } else { 2 } {
cpu.step(bus).unwrap();
}
cpu.registers()[1]
}
fn assert_readback(machine: &mut Machine, expected: u32) {
for thumb in [false, true] {
for base in [0, 0x3ffc] {
assert_eq!(
probe(machine.memory_mut(), thumb, base, 0xe590_1000, 0x6801),
expected
);
for lane in 0..4 {
let byte = (expected >> (lane * 8)) & 0xff;
assert_eq!(
probe(
machine.memory_mut(),
thumb,
base + lane,
0xe5d0_1000,
0x7801
),
byte
);
assert_eq!(
probe(
machine.memory_mut(),
thumb,
base + lane,
0xe1d0_10d0,
0x5681
),
byte as u8 as i8 as i32 as u32
);
}
for lane in [0, 2] {
let half = (expected >> (lane * 8)) & 0xffff;
assert_eq!(
probe(
machine.memory_mut(),
thumb,
base + lane,
0xe1d0_10b0,
0x8801
),
half
);
assert_eq!(
probe(
machine.memory_mut(),
thumb,
base + lane,
0xe1d0_10f0,
0x5e81
),
half as u16 as i16 as i32 as u32
);
}
}
}
}
// Return the actual last BIOS instruction, not a fixed firmware address.
fn reach_exit(machine: &mut Machine, target: u32, limit: usize) -> u32 {
let mut exit = None;
for _ in 0..limit {
if machine.cpu().pc() == target {
return exit.expect("must leave BIOS before target");
}
let pc = machine.cpu().pc();
machine.step().unwrap();
if pc < BIOS_SIZE as u32 && machine.cpu().pc() >= BIOS_SIZE as u32 {
exit = Some(pc);
}
}
panic!("did not reach {target:#x}");
}
fn assert_tail(machine: &Machine, exit: u32, expected: u32) {
assert_eq!(machine.memory().read32(exit + 4).unwrap(), UNSUPPORTED_TRAP);
assert_eq!(machine.memory().read32(exit + 8).unwrap(), expected);
}
#[test]
fn boot_readback_comes_from_guarded_firmware_tail() {
let mut machine = bios::boot(words(&[0xeaff_fffe])).unwrap();
let exit = reach_exit(&mut machine, ROM_START, 100);
assert_tail(&machine, exit, 0xe129_f000);
assert_readback(&mut machine, 0xe129_f000);
assert_eq!(machine.cpu().cpsr(), 0x1f);
assert_eq!(machine.cpu().registers()[13], SYSTEM_STACK);
}
#[test]
fn soft_reset_readback_matches_both_restart_targets_and_caller_states() {
for thumb in [false, true] {
for status in [0x10, 0x1f] {
for flag in [0, 1] {
let (mut machine, _) = call_status(0, thumb, [1, 2, 3], status);
machine.memory_mut().write8(0x0300_7ffa, flag).unwrap();
machine.step().unwrap();
let target = if flag == 0 { ROM_START } else { SOURCE };
let exit = reach_exit(&mut machine, target, 2000);
assert_tail(&machine, exit, 0xe129_f000);
assert_readback(&mut machine, 0xe129_f000);
assert_eq!(machine.cpu().cpsr(), 0x9f);
assert_eq!(&machine.cpu().registers()[..13], &[0; 13]);
assert_eq!(machine.cpu().registers()[13], SYSTEM_STACK);
assert_eq!(machine.cpu().registers()[14], target);
}
}
}
}
#[test]
fn returning_services_share_readback_without_changing_caller_status() {
for thumb in [false, true] {
for status in [0x10, 0x1f, 0x9f] {
for service in [1, 8, 0x0b, 0x0c] {
let (mut machine, target) = call_status(service, thumb, [0, 0, 0], status);
let before = machine.cpu().clone();
machine.step().unwrap();
let exit = reach_exit(&mut machine, target, 1000);
assert_tail(&machine, exit, 0xe3a0_2004);
assert_readback(&mut machine, 0xe3a0_2004);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
}
}
}
}
#[test]
fn irq_callback_and_return_have_distinct_image_derived_readback() {
for thumb in [false, true] {
for status in [0x10, 0x1f] {
let (mut machine, _) = call_status(8, thumb, [25, 2, 3], status);
let before = machine.cpu().clone();
callback(machine.memory_mut());
let prefix = CALLBACK + 0x80;
for (index, word) in [
0xe3a0_0000, // MOV r0,#0
0xe590_1000, // LDR r1,[r0] (protected BIOS read)
0xe59f_2008, // LDR r2,[pc,#8]
0xe582_1000, // STR r1,[r2]
0xe3a0_0301, // MOV r0,#I/O base
0xeaff_ffd9, // B CALLBACK (keep lr)
DEST,
]
.into_iter()
.enumerate()
{
machine
.memory_mut()
.write32(prefix + index as u32 * 4, word)
.unwrap();
}
machine.memory_mut().write32(IRQ_HANDLER, prefix).unwrap();
machine.memory_mut().write16(DISPSTAT, 8).unwrap();
machine.memory_mut().write16(IE, 1).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
machine.memory_mut().advance_cycles(VBLANK_START);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
let dispatch = reach_exit(&mut machine, prefix, 100);
assert_eq!(machine.memory().read32(dispatch + 4).unwrap(), 0xe8bd_500f);
assert_eq!(machine.memory().read32(dispatch + 8).unwrap(), 0xe25e_f004);
assert_readback(&mut machine, 0xe25e_f004);
let exit = reach_exit(&mut machine, before.pc(), 100);
assert_tail(&machine, exit, 0xe55e_c002);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0xe25e_f004);
assert_readback(&mut machine, 0xe55e_c002);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
assert_eq!(machine.cpu().registers(), before.registers());
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 1);
}
}
}
#[test]
fn interrupt_wait_replaces_irq_readback_with_swi_readback_on_return() {
for thumb in [false, true] {
let (mut machine, target) = call(5, thumb, [1, 2, 3]);
let before = machine.cpu().clone();
callback(machine.memory_mut());
machine.memory_mut().write16(DISPSTAT, 8).unwrap();
machine.memory_mut().write16(IE, 1).unwrap();
machine.step().unwrap();
let mut slept = false;
let mut irqs = 0;
let mut callback_seen = false;
for _ in 0..1000 {
if machine.cpu().pc() == target {
break;
}
if machine.cpu().pc() == CALLBACK {
callback_seen = true;
assert_readback(&mut machine, 0xe25e_f004);
}
match machine.step().unwrap() {
StepKind::HaltIdle => slept = true,
StepKind::IrqEntry => irqs += 1,
_ => {}
}
}
assert!(slept && callback_seen);
assert_eq!(irqs, 1);
assert_eq!(machine.cpu().pc(), target);
assert_readback(&mut machine, 0xe3a0_2004);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 0);
}
}
#[test]
fn supplied_image_tail_is_not_overridden_by_compatibility_constants() {
let rom = words(&[0xeaff_fffe]);
let mut original = bios::boot(rom.clone()).unwrap();
let exit = reach_exit(&mut original, ROM_START, 100);
let mut image = bios::image();
let value: u32 = 0x89ab_cdef;
image[exit as usize + 8..exit as usize + 12].copy_from_slice(&value.to_le_bytes());
let mut machine = Machine::new(Cpu::at_reset(), Memory::with_bios(rom, image).unwrap());
assert_eq!(reach_exit(&mut machine, ROM_START, 100), exit);
assert_readback(&mut machine, value);
assert_ne!(machine.memory().read32(0).unwrap(), value); // Host reads remain raw.
}