Stage 0: trap-frame parity across all 3 arches (FABRIC-3.md §XXVIII)
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s

First stage of the preemptive context-switching plan (see
~/.claude/plans/logical-snuggling-bear.md). Pure foundation work -- every
arch's ISR now saves the full register set on interrupt entry, so a trap
frame is in principle sufficient to resume execution anywhere it was
taken. No FORTH-visible behavior changes.

amd64: added FXSAVE/FXRSTOR, closing a genuine pre-existing correctness
gap (not just future-preemption prep) -- confirmed live double-precision
FP code reachable from ordinary interpreter dispatch (vm_runtime.c Loop
#5/#6), and the ISR previously saved zero FP/SSE state. rbp repurposed as
a fixed anchor so the 16-byte-aligned FXSAVE area can be carved out of an
unpredictably-aligned rsp without disturbing existing argument reads.

aarch64: extended the trap frame 672->800 bytes, adding v8-v15 (AAPCS64
callee-saved, previously excluded on call-site-only reasoning that
doesn't hold for an async trap).

riscv64: extended the trap frame 320->512 bytes, adding s0-s11 and
fs0-fs11 (the latter still correctly gated behind sstatus.FS != Off).

All 3 architectures re-verified clean boot to ok> under the new frames --
amd64 through hundreds of timer ticks with FXSAVE/FXRSTOR live on every
interrupt, aarch64 through 987 ticks, riscv64 clean on the now-larger
FS-conditional block.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UNhH1mhi52i6Qihh7ZV5S
This commit is contained in:
Robert Allan James
2026-09-13 09:23:28 -04:00
co-authored by Claude Sonnet 5
parent 2a30212bd3
commit 15672ce17c
12 changed files with 27368 additions and 39 deletions
+35 -14
View File
@@ -148,7 +148,7 @@ aarch64_install_vectors:
/* ---------------------------------------------------------------------------
* irq_spx_trampoline — save / call C handler / restore / ERET.
*
* Frame layout (672 bytes, 16-byte aligned; SP is guaranteed 16-aligned at
* Frame layout (800 bytes, 16-byte aligned; SP is guaranteed 16-aligned at
* any point AAPCS64 code can be asynchronously interrupted, so no extra
* alignment handling is needed around the initial SUB):
*
@@ -160,11 +160,20 @@ aarch64_install_vectors:
* 272 FPCR
* 280 (8 bytes unused -- keeps the V-register block 16-aligned)
* 288..415 v0-v7 (4 STP Q-pairs) -- AAPCS64 caller-saved
* 416..671 v16-v31 (8 STP Q-pairs) -- AAPCS64 caller-saved
* 416..543 v8-v15 (4 STP Q-pairs) -- AAPCS64 callee-saved
* 544..799 v16-v31 (8 STP Q-pairs) -- AAPCS64 caller-saved
*
* v8-v15 are AAPCS64 callee-saved and are deliberately not here: the C
* handler below is ordinary compiled code and preserves those itself if it
* touches them, per the same ABI the save list is drawn from.
* FABRIC-3.md SXXVIII (Stage 0, preemptive-context-switch trap-frame parity,
* 2026-09-13): v8-v15 now saved too. They are AAPCS64 callee-saved, so
* ordinary compiled code (the C handler below, or anything else called
* through a normal ABI boundary) already preserves them across a *call* --
* but an interrupt is not a call the interrupted code's own compiled
* instructions know about. A value the interrupted function is genuinely
* live-using in v8-v15 at the moment of interrupt has no ABI guarantee of
* surviving an async trap at all; only a frame that saves everything makes
* "resume execution anywhere it was taken" actually true. This trap frame
* is not yet copied into any per-VM saved context (that's Stage 2) -- this
* stage only makes the frame itself complete.
* ------------------------------------------------------------------------- */
.equ TF_X0, 0
.equ TF_X2, 16
@@ -190,15 +199,19 @@ aarch64_install_vectors:
.equ TF_V2, 320
.equ TF_V4, 352
.equ TF_V6, 384
.equ TF_V16, 416
.equ TF_V18, 448
.equ TF_V20, 480
.equ TF_V22, 512
.equ TF_V24, 544
.equ TF_V26, 576
.equ TF_V28, 608
.equ TF_V30, 640
.equ TF_SIZE, 672
.equ TF_V8, 416
.equ TF_V10, 448
.equ TF_V12, 480
.equ TF_V14, 512
.equ TF_V16, 544
.equ TF_V18, 576
.equ TF_V20, 608
.equ TF_V22, 640
.equ TF_V24, 672
.equ TF_V26, 704
.equ TF_V28, 736
.equ TF_V30, 768
.equ TF_SIZE, 800
.balign 4
irq_spx_trampoline:
@@ -247,6 +260,10 @@ irq_spx_trampoline:
stp q2, q3, [sp, #TF_V2]
stp q4, q5, [sp, #TF_V4]
stp q6, q7, [sp, #TF_V6]
stp q8, q9, [sp, #TF_V8]
stp q10, q11, [sp, #TF_V10]
stp q12, q13, [sp, #TF_V12]
stp q14, q15, [sp, #TF_V14]
stp q16, q17, [sp, #TF_V16]
stp q18, q19, [sp, #TF_V18]
stp q20, q21, [sp, #TF_V20]
@@ -262,6 +279,10 @@ irq_spx_trampoline:
ldp q2, q3, [sp, #TF_V2]
ldp q4, q5, [sp, #TF_V4]
ldp q6, q7, [sp, #TF_V6]
ldp q8, q9, [sp, #TF_V8]
ldp q10, q11, [sp, #TF_V10]
ldp q12, q13, [sp, #TF_V12]
ldp q14, q15, [sp, #TF_V14]
ldp q16, q17, [sp, #TF_V16]
ldp q18, q19, [sp, #TF_V18]
ldp q20, q21, [sp, #TF_V20]
+35 -7
View File
@@ -75,20 +75,48 @@ isr_common_entry:
* [rsp+152] = rflags
*/
/* FABRIC-3.md SXXVIII (Stage 0, preemptive-context-switch trap-frame
* parity, 2026-09-13): FXSAVE/FXRSTOR added here to close a real,
* pre-existing gap, not just a future-preemption nicety -- confirmed
* live double-precision FP code reachable from ordinary interpreter
* dispatch (vm_runtime.c's Loop #5/#6 physics inference, called from
* vm_tick() during word execution, not just at boot). The GPR pushes
* above only cover integer state; on x86-64 SysV all XMM registers are
* caller-saved, meaning the compiler is free to leave a live value in
* one between any two instructions of straight-line code -- an
* asynchronous interrupt is not a call site the interrupted code's own
* instructions know about, so nothing here previously guaranteed FP
* state actually survived a timer tick landing mid-computation.
*
* rbp is repurposed below as a fixed anchor pointing at "rsp right after
* the 15 GPR pushes" (rbp's own true pushed value already lives safely
* on the stack above and is restored by the ordinary `pop rbp` later) --
* FXSAVE requires a 16-byte-aligned address and rsp's alignment here is
* unpredictable (the interrupt could have landed anywhere), so the
* FXSAVE area is carved out and aligned separately, with rbp used to
* keep addressing everything else at its original, fixed offsets
* regardless of that adjustment. */
mov rbp, rsp
sub rsp, 528 /* 512-byte FXSAVE area + 16 slack for alignment */
and rsp, -16
fxsave [rsp]
/* Set up arguments for isr_common_handler(vector, error, rip, cs, rflags, cr2) */
mov rdi, [rsp + 120] /* vector */
mov rsi, [rsp + 128] /* error */
mov rdx, [rsp + 136] /* rip */
mov rcx, [rsp + 144] /* cs */
mov r8, [rsp + 152] /* rflags */
mov rdi, [rbp + 120] /* vector */
mov rsi, [rbp + 128] /* error */
mov rdx, [rbp + 136] /* rip */
mov rcx, [rbp + 144] /* cs */
mov r8, [rbp + 152] /* rflags */
mov r9, cr2 /* cr2 */
/* Pass stack frame pointer (after pushes) as 7th argument */
mov rax, rsp
push rax
push rbp
call isr_common_handler
add rsp, 8
fxrstor [rsp]
mov rsp, rbp /* collapse back to right after the GPR pushes */
/* Restore general-purpose registers */
pop r15
pop r14
+80 -17
View File
@@ -16,22 +16,33 @@
.hidden riscv64_install_vectors
/* ---------------------------------------------------------------------------
* Trap frame layout (offsets from sp), 320 bytes, 16-byte aligned.
* Trap frame layout (offsets from sp), 512 bytes, 16-byte aligned.
*
* Only the LP64D psABI's *caller-saved* registers are saved: a C handler
* preserves callee-saved registers itself, and the interrupted code already
* expects caller-saved registers to be clobbered across a call.
* FABRIC-3.md SXXVIII (Stage 0, preemptive-context-switch trap-frame parity,
* 2026-09-13): originally only the LP64D psABI's *caller-saved* registers
* were saved here, on the reasoning that a C handler preserves callee-saved
* registers itself. That reasoning holds for an ordinary function call, but
* not for an asynchronous trap: the interrupted code's own compiled
* instructions have no call site to justify losing state at, so a value
* genuinely live in a callee-saved register at the moment of interrupt has
* no ABI guarantee of surviving. s0-s11/fs0-fs11 are now saved too, so this
* frame is, in principle, sufficient to resume execution anywhere it was
* taken. Not yet copied into any per-VM saved context (that's Stage 2) --
* this stage only makes the frame itself complete.
*
* integer caller-saved (16): ra, t0-t6, a0-a7
* integer callee-saved (12): s0-s11
* FP caller-saved (20): ft0-ft11, fa0-fa7 + fcsr
* FP callee-saved (12): fs0-fs11
* supervisor CSRs (2): sepc, sstatus
*
* The FP half is saved only when sstatus.FS != Off. Nothing in boot.S or
* kernel_entry.S programs FS, so its state is whatever firmware left; touching
* an f-register while FS == Off raises an illegal-instruction trap, which
* inside the trap handler would be unrecoverable. The kernel image does
* contain real FP code (fld/fmul.d/fcvt on the Q48.16 and statistics paths),
* so the save cannot simply be omitted -- it has to be conditional.
* The FP half (caller- and callee-saved alike) is saved only when
* sstatus.FS != Off. Nothing in boot.S or kernel_entry.S programs FS, so its
* state is whatever firmware left; touching an f-register while FS == Off
* raises an illegal-instruction trap, which inside the trap handler would be
* unrecoverable. The kernel image does contain real FP code (fld/fmul.d/fcvt
* on the Q48.16 and statistics paths), so the save cannot simply be omitted
* -- it has to be conditional.
* ------------------------------------------------------------------------- */
.equ TF_RA, 0
.equ TF_T0, 8
@@ -49,13 +60,15 @@
.equ TF_A5, 104
.equ TF_A6, 112
.equ TF_A7, 120
.equ TF_SEPC, 128
.equ TF_SSTATUS,136
.equ TF_FCSR, 144
.equ TF_F0, 152 /* ft0-ft7 : 152 .. 208 */
.equ TF_FA0, 216 /* fa0-fa7 : 216 .. 272 */
.equ TF_FT8, 280 /* ft8-ft11 : 280 .. 304 */
.equ TF_SIZE, 320
.equ TF_S0, 128 /* s0-s11 : 128 .. 216 */
.equ TF_SEPC, 224
.equ TF_SSTATUS,232
.equ TF_FCSR, 240
.equ TF_F0, 248 /* ft0-ft7 : 248 .. 304 */
.equ TF_FA0, 312 /* fa0-fa7 : 312 .. 368 */
.equ TF_FT8, 376 /* ft8-ft11 : 376 .. 400 */
.equ TF_FS0, 408 /* fs0-fs11 : 408 .. 496 */
.equ TF_SIZE, 512
/* sstatus.FS occupies bits [14:13]; 0 = Off */
.equ SSTATUS_FS_SHIFT, 13
@@ -85,6 +98,19 @@ riscv64_trap_entry:
sd a6, TF_A6(sp)
sd a7, TF_A7(sp)
sd s0, TF_S0+0(sp)
sd s1, TF_S0+8(sp)
sd s2, TF_S0+16(sp)
sd s3, TF_S0+24(sp)
sd s4, TF_S0+32(sp)
sd s5, TF_S0+40(sp)
sd s6, TF_S0+48(sp)
sd s7, TF_S0+56(sp)
sd s8, TF_S0+64(sp)
sd s9, TF_S0+72(sp)
sd s10, TF_S0+80(sp)
sd s11, TF_S0+88(sp)
csrr t0, sepc
sd t0, TF_SEPC(sp)
csrr t0, sstatus
@@ -117,6 +143,18 @@ riscv64_trap_entry:
fsd ft9, TF_FT8+8(sp)
fsd ft10, TF_FT8+16(sp)
fsd ft11, TF_FT8+24(sp)
fsd fs0, TF_FS0+0(sp)
fsd fs1, TF_FS0+8(sp)
fsd fs2, TF_FS0+16(sp)
fsd fs3, TF_FS0+24(sp)
fsd fs4, TF_FS0+32(sp)
fsd fs5, TF_FS0+40(sp)
fsd fs6, TF_FS0+48(sp)
fsd fs7, TF_FS0+56(sp)
fsd fs8, TF_FS0+64(sp)
fsd fs9, TF_FS0+72(sp)
fsd fs10, TF_FS0+80(sp)
fsd fs11, TF_FS0+88(sp)
1:
/* scause bit 63 set => interrupt (negative when read as signed). */
@@ -161,6 +199,18 @@ riscv64_trap_entry:
fld ft9, TF_FT8+8(sp)
fld ft10, TF_FT8+16(sp)
fld ft11, TF_FT8+24(sp)
fld fs0, TF_FS0+0(sp)
fld fs1, TF_FS0+8(sp)
fld fs2, TF_FS0+16(sp)
fld fs3, TF_FS0+24(sp)
fld fs4, TF_FS0+32(sp)
fld fs5, TF_FS0+40(sp)
fld fs6, TF_FS0+48(sp)
fld fs7, TF_FS0+56(sp)
fld fs8, TF_FS0+64(sp)
fld fs9, TF_FS0+72(sp)
fld fs10, TF_FS0+80(sp)
fld fs11, TF_FS0+88(sp)
3:
csrw sstatus, t0
ld t0, TF_SEPC(sp)
@@ -183,6 +233,19 @@ riscv64_trap_entry:
ld a6, TF_A6(sp)
ld a7, TF_A7(sp)
ld s0, TF_S0+0(sp)
ld s1, TF_S0+8(sp)
ld s2, TF_S0+16(sp)
ld s3, TF_S0+24(sp)
ld s4, TF_S0+32(sp)
ld s5, TF_S0+40(sp)
ld s6, TF_S0+48(sp)
ld s7, TF_S0+56(sp)
ld s8, TF_S0+64(sp)
ld s9, TF_S0+72(sp)
ld s10, TF_S0+80(sp)
ld s11, TF_S0+88(sp)
addi sp, sp, TF_SIZE
sret