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:
0xrsydn 2026-10-06 00:37:20 +07:00
commit efdc288db6
15 changed files with 387 additions and 34 deletions

View file

@ -45,7 +45,8 @@ pub fn boot(rom: Vec<u8>) -> Result<Machine, MemoryError> {
}
/// Build a deterministic 16 KiB image from original ARM instructions and a
/// mathematically generated sine table. No Nintendo firmware bytes are included.
/// mathematically generated sine table. Documented protected-read compatibility
/// words are explicit exit data; no Nintendo firmware routines are included.
/// SWIs use bits 16–23 of the ARM immediate, or the Thumb immediate byte.
/// Services support User/System callers. Returning services preserve caller status
/// and registers other than each arithmetic service's documented outputs.
@ -90,6 +91,7 @@ pub fn image() -> Vec<u8> {
a.emit(0xe5c1_0000); // STRB r0,[r1]
a.emit(0xe321_f01f); // MSR CPSR_c,#System
a.literal(15, ROM_START); // LDR pc,=ROM_START
a.prefetch_tail(0xe129_f000); // Boot / SoftReset protected-read word.
a.label("irq");
a.emit(0xe92d_500f); // STMDB sp!,{r0-r3,r12,lr}
@ -103,7 +105,10 @@ pub fn image() -> Vec<u8> {
a.emit(0xe1a0_e00f); // MOV lr,pc (return after BX)
a.emit(0xe12f_ff11); // BX r1
a.emit(0xe8bd_500f); // LDMIA sp!,{r0-r3,r12,lr}
// This instruction is also the callback branch's PC+8 readback word.
a.emit(0xe25e_f004); // SUBS pc,lr,#4
a.prefetch_tail(0xe55e_c002); // IRQ return protected-read word.
a.label("swi");
a.emit(0xe92d_500f); // STMDB sp!,{r0-r3,r12,lr}
@ -268,6 +273,7 @@ pub fn image() -> Vec<u8> {
a.emit(0xe169_f003); // MSR SPSR_fc,r3
a.emit(0xe8bd_500f); // LDMIA sp!,{r0-r3,r12,lr}
a.emit(0xe1b0_f00e); // MOVS pc,lr
a.prefetch_tail(0xe3a0_2004); // SWI return protected-read word.
a.label("unsupported");
a.emit(UNSUPPORTED_TRAP);
a.label("invalid_argument");
@ -331,6 +337,14 @@ impl ArmImage {
self.code.push(instruction);
}
/// Place only after a non-fallthrough exit. Neither word is executed.
/// The normal ARM PC+8 snapshot exposes this documented compatibility data.
/// Keep the bus image-derived, including for caller-supplied BIOS images.
fn prefetch_tail(&mut self, value: u32) {
self.emit(UNSUPPORTED_TRAP); // PC+4: diagnose accidental fallthrough.
self.emit(value); // PC+8: retained BIOS word after the exit.
}
fn label(&mut self, name: &'static str) {
assert!(
self.labels.insert(name, self.code.len()).is_none(),
@ -418,6 +432,32 @@ mod image_tests {
);
}
#[test]
fn prefetch_tail_keeps_branch_and_literal_fixups_outside_compatibility_data() {
let mut a = ArmImage::default();
a.literal(0, 0x1122_3344);
a.branch(14, "after_tail");
a.prefetch_tail(0xe129_f000);
a.label("after_tail");
a.emit(0xe12f_ff1e); // BX lr
let image = a.finish(BIOS_SIZE);
let actual: Vec<_> = image[..24]
.chunks_exact(4)
.map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap()))
.collect();
assert_eq!(
actual,
[
0xe59f_000c, // Literal at 20, relative to PC+8.
0xea00_0001, // Branch at 4 skips both tail words to 16.
UNSUPPORTED_TRAP,
0xe129_f000,
0xe12f_ff1e,
0x1122_3344,
]
);
}
#[test]
#[should_panic(expected = "BIOS code overlaps reserved data")]
fn code_cannot_overlap_reserved_tables() {

View file

@ -40,6 +40,7 @@ pub(super) fn emit(a: &mut ArmImage) {
// IME, IE, IF, devices, or unrelated CPU banks. IRQ stays masked throughout.
a.emit(0xe329_f09f); // MSR CPSR_fc,#System|I
a.emit(0xe12f_ff1e); // BX lr (ARM target, no return through erased SWI frame)
a.prefetch_tail(0xe129_f000); // Same protected-read word as cold boot.
emit_ram_reset(a);
}