/* * 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