Files
LithosAnanake/kernel/src/arch/amd64/switch.S
T
rajamesandClaude Opus 5.5 029a8ed9cf fix(hermes): one delivery path -- a VM takes its own messages on its own turn, never on another's stack
FABRIC-3.5 SXXXII, closing FABRIC-3 SXXIX bug 2 (SXXXVI).  A send marks the
receiver for the switch signal; the VM's own loop on its native stack takes
its messages and yields; Hera's idle beat makes the pending switch instead
of running other VMs' messages on her stack; every VM gets switcher turns
from birth; the drain refuses any call outside the VM's own context (the
test: it held every relayed line before, none now).

Found on the way: amd64's first-entry frame left rsp 8 bytes off the SysV
entry alignment (#GP on the first interpreter run on a VM's own stack), and
a VM yielding from its loop was marked SWITCHED_OUT, which stopped console
relay after one message and would have made KILL refuse it.

Relay of four lines to a WIREBIND identity on all three ISAs, five
hot-plugged identities, and standard acceptance (POST 1052, hash
0x08873e0f44b7cb2a) all pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:37:57 -04:00

88 lines
3.4 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
/* 64, not 56 (2026-10-14): one more slot above the return address,
* where a caller's own return address would sit. `ret` into `entry`
* then leaves rsp at stack_top - 8, i.e. rsp + 8 a multiple of 16,
* exactly as after a real `call` -- the SysV ABI's entry alignment.
* With 56, entry began 8 bytes off: harmless while the entry function
* only switched back, a #GP (an aligned SSE store to the stack) the
* first time it ran the interpreter on this stack. */
sub rax, 64
mov qword ptr [rax+56], 0 /* no caller: a null return address */
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