From 6a8e84b64b1dc9b120cf5998f6de7b08c1bb1655 Mon Sep 17 00:00:00 2001 From: Bernie Innocenti Date: Fri, 23 Oct 2026 18:37:25 +0900 Subject: [PATCH] exec.library: Supervisor enters its routine through a real exception frame The 6-byte 68000 frame broke every Supervisor routine on a 68010 or later, whose RTE reads a format word; switch to the supervisor stack as exception entry does, and return straight to the caller, so a routine may RTE elsewhere (ixemul's longjmp). Co-authored-by: Claude Opus 5.5 --- crates/volamos-core/src/backend.rs | 25 ++++ crates/volamos-core/src/cpu.rs | 9 ++ crates/volamos-core/src/dispatch.rs | 76 +---------- crates/volamos-core/src/execfmt.rs | 195 +++++++++++++++++++++------- 4 files changed, 190 insertions(+), 115 deletions(-) diff --git a/crates/volamos-core/src/backend.rs b/crates/volamos-core/src/backend.rs index 3d87286..aa0f115 100644 --- a/crates/volamos-core/src/backend.rs +++ b/crates/volamos-core/src/backend.rs @@ -893,6 +893,31 @@ impl Cpu for M68kCpu { true } + fn push_exception_frame(&mut self, mem: &mut Self::Memory, return_pc: u32) { + let old_sr = self.core.get_sr(); + // Supervisor mode, on the supervisor stack, without trace: what + // exception entry does before it writes the frame. + self.core.set_sr((old_sr | 0x2000) & !0xC000); + let top = self.address_register(AddressRegister(7)); + let size = if self.core.cpu_type == CpuType::M68000 { + 6 + } else { + 8 + }; + let sp = top.wrapping_sub(size); + self.set_address_register(AddressRegister(7), sp); + // Publish the push to the sanitizer before writing the frame, + // as its own per-instruction hook does for a guest push. + if let Some(shadow) = mem.shadow_mut() { + shadow.update_stack_pointer(sp); + } + AddressSpace::write_u16(mem, sp, old_sr); + AddressSpace::write_u32(mem, sp.wrapping_add(2), return_pc); + if size == 8 { + AddressSpace::write_u16(mem, sp.wrapping_add(6), 0x0020); + } + } + fn emulated_cycles(&self) -> u64 { self.cycles } diff --git a/crates/volamos-core/src/cpu.rs b/crates/volamos-core/src/cpu.rs index 38690c8..f30a89e 100644 --- a/crates/volamos-core/src/cpu.rs +++ b/crates/volamos-core/src/cpu.rs @@ -203,6 +203,15 @@ pub trait Cpu { /// this method. fn take_hardware_exception(&mut self, mem: &mut Self::Memory, kind: TrapKind) -> bool; + /// Enters supervisor mode the way the CPU enters an exception, with + /// a frame that returns to `return_pc` in the current `SR`: switches + /// to the supervisor stack (clearing the trace bits), then pushes the + /// frame the emulated model's own `RTE` pops -- `SR` and `PC` on a + /// 68000, plus a format 0 vector word (`$20`, privilege violation) on + /// a 68010 and later. `exec.library`'s `Supervisor` enters its routine + /// through this, see `crate::execfmt::supervisor_handler`. + fn push_exception_frame(&mut self, mem: &mut Self::Memory, return_pc: u32); + /// Accumulated real m68k CPU cycles consumed so far, for a backend /// that tracks them at all -- see /// [`crate::backend::M68kCpu::set_clock_mhz`] (the CLI's diff --git a/crates/volamos-core/src/dispatch.rs b/crates/volamos-core/src/dispatch.rs index 4a0c458..2f574bf 100644 --- a/crates/volamos-core/src/dispatch.rs +++ b/crates/volamos-core/src/dispatch.rs @@ -1591,14 +1591,11 @@ type Continuation = Box) -> Result<(), /// [`LibraryTable::dispatch`] without reentering [`Runtime::run`] itself. /// The trampoline sidesteps the problem entirely by not stepping at all: /// it lets the *existing* dispatch loop do the work, split across two -/// (or more) handler invocations instead of one. Both call sites have -/// since been migrated onto this primitive (issue #192) -- -/// `call_put_ch_proc`'s private loop was replaced by a chain of -/// trampolined calls, one per output character, and `supervisor_handler` -/// by [`ContinuationStack::trampoline_rte`], a variant of this primitive -/// that accounts for `Supervisor`'s real contract (the routine terminates -/// with `RTE`, not `RTS`) -- see that method's own doc for how it adapts -/// this mechanism's `RTS`-based convention to an `RTE`-based one. +/// (or more) handler invocations instead of one. `call_put_ch_proc`'s +/// private loop was replaced by a chain of trampolined calls, one per +/// output character (issue #192). `supervisor_handler`, the other one, +/// no longer needs a continuation at all: it enters its routine through +/// an exception frame that returns straight to the caller. /// /// # Mechanism /// @@ -1725,69 +1722,6 @@ impl ContinuationStack { self.pending.push(Box::new(continuation)); } - - /// Like [`ContinuationStack::trampoline`], but arranges for the - /// subroutine to return via a real `RTE` instead of an `RTS` -- - /// `exec.library`'s `Supervisor` is the one real AmigaOS contract this - /// runtime implements where the callee's own documented return - /// instruction is `RTE`, not `RTS` (see `crate::execfmt:: - /// supervisor_handler`'s doc for the full real-hardware rationale). - /// - /// [`ContinuationStack::trampoline`] pushes a plain 4-byte return - /// address (`RTS`'s own pop shape); an `RTE`-terminated routine - /// instead pops a real 6-byte m68k exception-style frame -- a `u16` - /// `SR` at the lower address followed by a `u32` `PC` (matching every - /// other real exception frame this runtime delivers, see - /// `Cpu::take_hardware_exception`'s doc). This pushes exactly that - /// frame, with `PC` set to [`CONTINUATION_STUB_ADDR`] so the - /// subroutine's `rte` lands back on the trampoline stub exactly like - /// an `RTS`-terminated one would, then pushes `subroutine_entry` on - /// top of it so [`Runtime::run`]'s generic post-dispatch `rts` (for - /// *this* handler's own return) still enters the subroutine the same - /// way `trampoline` does -- everything past that first entry is - /// identical to the plain `RTS` case; only the frame shape the - /// subroutine's own return instruction consumes differs. In - /// particular, no special-casing is needed in `Runtime::run` itself: - /// the subroutine's `rte` is an ordinary instruction the CPU core - /// executes on its own, and it lands PC on [`CONTINUATION_STUB_ADDR`] - /// exactly as if an `RTS` had popped it there, so the very next step - /// traps into [`continuation_stub_handler`] through the completely - /// ordinary trap-dispatch path, same as any other library call. - /// - /// `sr` is the status-register value the subroutine's `RTE` will - /// restore once it returns -- ordinarily the caller's own current - /// `SR`, unchanged, since this runtime has no real supervisor/user - /// privilege distinction to elevate (every guest instruction already - /// executes as if privileged); see `supervisor_handler`'s doc for why - /// passing the caller's own `SR` straight through is the right choice - /// rather than synthesizing a different one. - pub fn trampoline_rte( - &mut self, - cpu: &mut C, - mem: &mut C::Memory, - subroutine_entry: u32, - sr: u16, - continuation: F, - ) where - F: FnOnce(&mut HandlerContext<'_, C>) -> Result<(), DispatchError> + 'static, - { - let sp = cpu.address_register(AddressRegister(7)); - // Push the RTE-style exception frame first (deeper on the stack) - // so it's what the subroutine's own `rte` pops once it has - // consumed its own local pushes -- mirroring `trampoline`'s - // "push CONTINUATION_STUB_ADDR first" ordering, just with a - // 6-byte frame instead of a bare 4-byte address. - let sp = sp.wrapping_sub(6); - mem.write_u16(sp, sr); - mem.write_u32(sp.wrapping_add(2), CONTINUATION_STUB_ADDR); - // Then push subroutine_entry on top: the very next thing - // Runtime::run's post-dispatch rts (for *this* handler) pops. - let sp = sp.wrapping_sub(4); - mem.write_u32(sp, subroutine_entry); - cpu.set_address_register(AddressRegister(7), sp); - - self.pending.push(Box::new(continuation)); - } } /// The stub handler bound to [`CONTINUATION_SLOT`] at diff --git a/crates/volamos-core/src/execfmt.rs b/crates/volamos-core/src/execfmt.rs index 73589b1..53e15e7 100644 --- a/crates/volamos-core/src/execfmt.rs +++ b/crates/volamos-core/src/execfmt.rs @@ -419,54 +419,39 @@ fn raw_do_fmt_handler( /// implementation here pushed a plain `RTS`-style return address, and /// the real `CPU` command's routine promptly executed a bare `rte` /// with nothing valid on the stack, sending the program counter -/// straight off the end of guest memory). This runtime has no user/ -/// supervisor privilege distinction at all (every guest instruction -/// already executes as if privileged), so there's nothing to actually -/// elevate -- but the routine's own `RTE` still needs a real -/// exception-style stack frame (`SR` then `PC`, 6 bytes, matching every -/// other real 68000 exception this runtime delivers -- see -/// [`crate::cpu::Cpu::take_hardware_exception`]'s doc) to pop. +/// straight off the end of guest memory). /// -/// # Trampolined, not stepped (issue #192) -/// -/// An earlier implementation here stepped the CPU itself, in a private -/// loop, exactly like `call_put_ch_proc` used to for `RawDoFmt` -- and -/// inherited the identical limitation: a routine that made any further -/// library call of its own had no way to have that call routed through -/// [`crate::dispatch::LibraryTable::dispatch`] from inside a nested -/// `Cpu::step` loop, and the private loop reported it as an error instead. -/// `ixemul.library`'s own `expunge()` cleanup -- which every ixemul-linked -/// program runs on exit -- is a real, non-buggy routine that hits exactly -/// this: it calls `Supervisor` to restore a bus-error vector, and -/// something reached from there makes an ordinary library call. -/// -/// This now uses [`crate::dispatch::ContinuationStack::trampoline_rte`], -/// the `RTE`-aware sibling of the trampoline primitive several other -/// handlers already use (see that method's doc for exactly how it adapts -/// the primitive's `RTS`-based convention to `Supervisor`'s `RTE`-based -/// one): it pushes the routine's own entry point plus the exception-style -/// frame its `RTE` will pop, and queues a continuation to run once that -/// happens. The continuation here is trivial -- an empty `Ok(())` -- -/// because the frame's `PC` already lands back on the real caller's own -/// return address once the routine's `RTE` and this handler's own -/// synthetic `rts` have both been consumed (see `trampoline_rte`'s doc -/// for why no register ever needs restoring by hand here): the real -/// caller's `D0` is simply whatever the routine itself left there, -/// exactly the documented `Supervisor` result, and every other register -/// the routine didn't touch was never touched by this handler either. -/// Found missing while running the real `CPU` command (Workbench 3.1.4 -/// `C:`), which wraps its direct `CACR`-register access in `Supervisor` -/// (a real, privileged `movec` instruction on 68020+) even though -/// `CacheControl` already answers its query -- defensive real-world -/// code, not a bug. +/// The routine is entered the way real `Supervisor` enters it, through +/// an exception: [`Cpu::push_exception_frame`] switches to supervisor +/// mode and the supervisor stack and pushes the frame the emulated CPU +/// model's own `RTE` pops (a format 0 frame on a 68010 and later, whose +/// `RTE` reads a format word the 68000's 6-byte frame does not have), +/// returning straight to the caller in its own `SR`. Nothing in this +/// runtime waits for the routine to return: it runs in the ordinary +/// dispatch loop, so it can make library calls of its own (issue #192: +/// `ixemul.library`'s `expunge()` cleanup does), and its `RTE` may go +/// somewhere else entirely -- `ixemul.library`'s `longjmp()` is a +/// `Supervisor` routine that rewrites the frame's `PC` and `SR` and sets +/// `USP`, so that its `RTE` lands at the `setjmp()` in user mode. fn supervisor_handler( ctx: &mut HandlerContext<'_, C>, ) -> Result<(), DispatchError> { let routine = ctx.cpu.address_register(AddressRegister(5)); - let saved_sr = ctx.cpu.sr(); - - ctx.continuations - .trampoline_rte(ctx.cpu, ctx.mem, routine, saved_sr, |_ctx| Ok(())); + // The caller's `jsr -30(a6)` left its return address on top of its + // stack, where Runtime::run's post-dispatch `rts` would pop it: it + // goes into the exception frame instead, for the routine's `RTE`. + let sp = ctx.cpu.address_register(AddressRegister(7)); + let return_pc = ctx.mem.read_u32(sp); + ctx.cpu + .set_address_register(AddressRegister(7), sp.wrapping_add(4)); + ctx.cpu.push_exception_frame(ctx.mem, return_pc); + // The post-dispatch `rts` enters the routine, on top of the frame. + let sp = ctx.cpu.address_register(AddressRegister(7)).wrapping_sub(4); + ctx.cpu.set_address_register(AddressRegister(7), sp); + if let Some(shadow) = ctx.mem.shadow_mut() { + shadow.update_stack_pointer(sp); + } + ctx.mem.write_u32(sp, routine); Ok(()) } @@ -644,10 +629,14 @@ mod tests { } fn runtime_with_program(words: &[u16]) -> Runtime { + runtime_with_program_on(words, M68kCpu::new()) + } + + fn runtime_with_program_on(words: &[u16], cpu: M68kCpu) -> Runtime { let mut mem = FlatMemory::new(0x2_0000); load_words(&mut mem, TRAP_TABLE_END, words); Runtime::new( - M68kCpu::new(), + cpu, mem, StartConfig { entry: TRAP_TABLE_END, @@ -852,4 +841,122 @@ mod tests { "RawMayGetChar's documented D0=-1 result, negated, proves the nested call really ran" ); } + + /// `moveq #42,d0 ; rte` through `Supervisor`, on `cpu`: the exit code. + fn run_supervisor_moveq_42(cpu: M68kCpu) -> i32 { + let mut words = movea_exec_base_to_a6().to_vec(); + let routine_idx = words.len(); + words.push(move_imm_to_a(5)); // A5 = routine (patched) + words.push(0); + words.push(0); + words.extend_from_slice(&jsr_disp16_a6(-30)); // Supervisor(a6) + words.push(RTS); + let routine_addr = TRAP_TABLE_END + (words.len() as u32) * 2; + words.extend_from_slice(&[0x702A, 0x4E73]); // moveq #42,d0 ; rte + words[routine_idx + 1] = (routine_addr >> 16) as u16; + words[routine_idx + 2] = routine_addr as u16; + let mut rt = runtime_with_program_on(&words, cpu); + let mut out = Vec::new(); + rt.run(&mut out, None).expect("run should succeed") + } + + #[test] + fn supervisor_pushes_the_frame_a_68020_rte_pops() { + // A 68010 or later RTE reads a format word after the PC, which + // the 68000's 6-byte frame does not have. + assert_eq!(run_supervisor_moveq_42(M68kCpu::new()), 42); + for cpu_type in [ + m68k::CpuType::M68010, + m68k::CpuType::M68020, + m68k::CpuType::M68040, + ] { + assert_eq!( + run_supervisor_moveq_42(M68kCpu::with_config(cpu_type, false)), + 42, + "{cpu_type:?}" + ); + } + } + + /// A routine that rewrites its frame's `PC` and `RTE`s there instead + /// of back to the caller -- what `ixemul.library`'s `longjmp()` does + /// -- then a second `Supervisor` call from the new place, which must + /// not be confused by the first never returning. + fn run_supervisor_rte_elsewhere(cpu: M68kCpu) -> i32 { + let mut words = movea_exec_base_to_a6().to_vec(); + let routine_idx = words.len(); + words.push(move_imm_to_a(5)); // A5 = routine (patched) + words.push(0); + words.push(0); + words.extend_from_slice(&jsr_disp16_a6(-30)); // Supervisor(a6) + words.push(0x7001); // moveq #1,d0 -- not reached + words.push(RTS); + + // routine: move.l #target,2(sp) ; rte + let routine_addr = TRAP_TABLE_END + (words.len() as u32) * 2; + let target_patch = words.len() + 1; + words.extend_from_slice(&[0x2F7C, 0, 0, 0x0002, 0x4E73]); + + // target: a second Supervisor call, whose routine returns 9 + let target_addr = TRAP_TABLE_END + (words.len() as u32) * 2; + let routine2_idx = words.len(); + words.push(move_imm_to_a(5)); // A5 = routine2 (patched) + words.push(0); + words.push(0); + words.extend_from_slice(&jsr_disp16_a6(-30)); // Supervisor(a6) + words.push(RTS); + let routine2_addr = TRAP_TABLE_END + (words.len() as u32) * 2; + words.extend_from_slice(&[0x7009, 0x4E73]); // moveq #9,d0 ; rte + + words[routine_idx + 1] = (routine_addr >> 16) as u16; + words[routine_idx + 2] = routine_addr as u16; + words[target_patch] = (target_addr >> 16) as u16; + words[target_patch + 1] = target_addr as u16; + words[routine2_idx + 1] = (routine2_addr >> 16) as u16; + words[routine2_idx + 2] = routine2_addr as u16; + let mut rt = runtime_with_program_on(&words, cpu); + let mut out = Vec::new(); + rt.run(&mut out, None).expect("run should succeed") + } + + #[test] + fn supervisor_routine_may_rte_somewhere_else() { + assert_eq!(run_supervisor_rte_elsewhere(M68kCpu::new()), 9); + assert_eq!( + run_supervisor_rte_elsewhere(M68kCpu::with_config(m68k::CpuType::M68020, false)), + 9 + ); + } + + #[test] + fn supervisor_from_user_mode_runs_the_routine_in_supervisor_mode() { + // Drop to user mode on a stack of its own (USP), call Supervisor, + // and return the routine's SR & S: the routine must run in + // supervisor mode, and its RTE must bring the program back to user + // mode on the user stack, where the final rts finds its address. + let mut words = movea_exec_base_to_a6().to_vec(); + words.extend_from_slice(&[ + 0x2417, // move.l (sp),d2 -- the program's return address + 0x204F, // movea.l sp,a0 + 0x90FC, 0x0400, // suba.w #$400,a0 + 0x2102, // move.l d2,-(a0) + 0x4E60, // move.l a0,usp + 0x027C, 0xDFFF, // andi.w #$dfff,sr -- user mode, A7 = USP + ]); + let routine_idx = words.len(); + words.push(move_imm_to_a(5)); // A5 = routine (patched) + words.push(0); + words.push(0); + words.extend_from_slice(&jsr_disp16_a6(-30)); // Supervisor(a6) + words.push(RTS); + let routine_addr = TRAP_TABLE_END + (words.len() as u32) * 2; + // move.w sr,d0 ; andi.l #$2000,d0 ; rte + words.extend_from_slice(&[0x40C0, 0x0280, 0x0000, 0x2000, 0x4E73]); + words[routine_idx + 1] = (routine_addr >> 16) as u16; + words[routine_idx + 2] = routine_addr as u16; + let mut rt = runtime_with_program(&words); + let mut out = Vec::new(); + let code = rt.run(&mut out, None).expect("run should succeed"); + assert_eq!(code, 0x2000, "the routine ran with the S bit set"); + } }