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.
This commit is contained in:
parent
b16588aa4c
commit
efdc288db6
15 changed files with 387 additions and 34 deletions
|
|
@ -28,6 +28,8 @@ mod huffman;
|
|||
mod lz77;
|
||||
#[path = "bios/ram_reset.rs"]
|
||||
mod ram_reset;
|
||||
#[path = "bios/readback.rs"]
|
||||
mod readback;
|
||||
#[path = "bios/reset.rs"]
|
||||
mod reset;
|
||||
#[path = "bios/run_length.rs"]
|
||||
|
|
|
|||
253
crates/gba-core/tests/bios/readback.rs
Normal file
253
crates/gba-core/tests/bios/readback.rs
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
//! 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.
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue