sk_vm_switch_entry() stops being the Stage-2 placeholder bounce and becomes every switch participant's own idle loop: it drains one of its own kernel-Hermes messages per pass, on its own native stack (driven by the drain's return value, so a non-consuming drain cannot spin), and when nothing is drained it marks the VM parked_idle and yields back. It is entirely OS-driven, with no VM-side yield word. sk_hermes_drain_checkpoint() gains a per-VM hermes_draining guard so a payload's own in-word checkpoint cannot drain a second message inside the first. Still inert in normal operation: nothing marks work until Stage C. Finding fixed here: amd64 sk_vm_switch_prime built the first-entry frame at top-56, entering with rsp 16-aligned instead of the SysV-required 8 mod 16, so the first aligned SSE spill in a callee took #GP (logs/20260923-142526, kept). It had been latent since SXXVIII Stage 2 because the placeholder never called anything. The frame is now at top-64 with a zeroed pad slot. aarch64 and riscv64 were already correct. Evidence: a temporary kernel_main probe (reverted, not in this commit) queued two messages to Hestia, the first calling a colon word, and switched into her. It passed on all three ISAs: 0 pending, parked_idle, depth 0, context saved on her own stack, SB-SEQ=12 (drained one at a time, not nested), ledger and stadium_conserved restored. Formal acceptance without the probe on all three ISAs: dict_hash identical and unchanged, zero UNKNOWN WORD or faults, and the live WIREBIND mint -> USE -> 1 2 + . -> 3 relay. FABRIC-3.8.md also records Captain Bob's rulings that no VM-side word ever gives up control and that multitasking is a compudynamic hybrid, and opens SS3.12 (VM frames for per-VM execution state). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89 lines
3.6 KiB
ArmAsm
89 lines
3.6 KiB
ArmAsm
/*
|
|
* switch.S (amd64) - Cooperative VM context switch primitive.
|
|
*
|
|
* FABRIC-3.md §XXVIII (Stage 2, preemptive-context-switch save/restore
|
|
* primitive, 2026-09-13). This is deliberately NOT the same mechanism as
|
|
* isr.S's interrupt trap frame (Stage 0) -- that saves every register
|
|
* because an interrupt is not a call site the interrupted code knows
|
|
* about. sk_vm_switch_to() IS an ordinary function call (SWITCH-TO's C
|
|
* handler calls it directly), so the standard System V AMD64 ABI already
|
|
* guarantees every caller-saved register (rax, rcx, rdx, rsi, rdi, r8-r11,
|
|
* all XMM) is the caller's own problem to preserve across the call, exactly
|
|
* like any other call. Only the callee-saved set -- rbx, rbp, r12-r15 --
|
|
* needs explicit handling here, because this function does not really
|
|
* "return" in the normal sense: it swaps to a completely different stack
|
|
* and (from the newly-resumed context's point of view) appears to return
|
|
* from whichever earlier call to this same function originally parked it.
|
|
* SysV has no callee-saved XMM registers at all, so there is no FP state
|
|
* to carry here.
|
|
*
|
|
* void sk_vm_switch_to(uint64_t *save_sp_out, uint64_t new_sp);
|
|
* rdi = save_sp_out, rsi = new_sp
|
|
*
|
|
* For a VM that has never run before, new_sp must point at a synthetic
|
|
* frame built to this exact layout by the C glue (switch.c): six zeroed
|
|
* quadwords (r15, r14, r13, r12, rbx, rbp -- values irrelevant, never a
|
|
* real prior context) followed by the trampoline entry address at
|
|
* new_sp+48, so the final `ret` below jumps there instead of into some
|
|
* prior caller.
|
|
*/
|
|
|
|
.intel_syntax noprefix
|
|
.section .text
|
|
|
|
.hidden sk_vm_switch_to
|
|
|
|
.global sk_vm_switch_to
|
|
sk_vm_switch_to:
|
|
push rbp
|
|
push rbx
|
|
push r12
|
|
push r13
|
|
push r14
|
|
push r15
|
|
|
|
mov [rdi], rsp /* *save_sp_out = rsp (this context's parked SP) */
|
|
mov rsp, rsi /* rsp = new_sp (switch stacks) */
|
|
|
|
pop r15
|
|
pop r14
|
|
pop r13
|
|
pop r12
|
|
pop rbx
|
|
pop rbp
|
|
ret
|
|
|
|
/*
|
|
* uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry);
|
|
*
|
|
* Builds the synthetic frame described above at the top of a fresh native
|
|
* stack, so the first ever sk_vm_switch_to() into it lands at `entry`
|
|
* instead of some real prior caller. Returns the resulting sp value to
|
|
* pass as sk_vm_switch_to()'s new_sp. Kept in this file (not C) so the
|
|
* layout can never drift from what sk_vm_switch_to() above actually
|
|
* expects -- one set of offsets, not two copies to keep in sync.
|
|
*/
|
|
.hidden sk_vm_switch_prime
|
|
|
|
.global sk_vm_switch_prime
|
|
sk_vm_switch_prime:
|
|
mov rax, rdi
|
|
/* FABRIC-3.8.md Stage B finding (2026-09-23): was `sub rax, 56`, which
|
|
* entered `entry` with rsp == stack_top (16-byte aligned). SysV requires
|
|
* rsp == 8 (mod 16) at function entry (as if a `call` had just pushed a
|
|
* return address); off by 8, any compiler-emitted aligned SSE spill in
|
|
* the entry function's callees #GPs. Latent through Stage 2 because the
|
|
* placeholder trampoline never called anything that spilled; found live
|
|
* the moment the trampoline started draining (vm_interpret()). 64 leaves
|
|
* a zeroed pad slot above the entry address so entry sees rsp = top-8. */
|
|
sub rax, 64
|
|
mov qword ptr [rax+56], 0 /* pad: the fake caller's slot, never used */
|
|
mov qword ptr [rax+0], 0 /* r15 */
|
|
mov qword ptr [rax+8], 0 /* r14 */
|
|
mov qword ptr [rax+16], 0 /* r13 */
|
|
mov qword ptr [rax+24], 0 /* r12 */
|
|
mov qword ptr [rax+32], 0 /* rbx */
|
|
mov qword ptr [rax+40], 0 /* rbp */
|
|
mov [rax+48], rsi /* return address = entry */
|
|
ret
|