From e98a17d66d2232fda890193b81378b48c3f9605b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 17:11:28 +0000 Subject: [PATCH 01/11] Spill FPSCR with the ARM hard-float stack image. Linux saves and restores the whole FPSCR on a VFP context switch. stackman_switch stores it in an 8-byte slot below the pointer passed to the callback and writes it back after the restore callback, ahead of the d8-d15 pop. The caller-saved flag bits travel with that copy. stackman_call does not change FPSCR. --- AGENTS.md | 2 +- docs/abi-register-audit.md | 2 +- stackman/platforms/switch_armhf_gcc.S | 23 +++++++++++++++++------ stackman/platforms/switch_armhf_gcc.h | 25 ++++++++++++++++++++++--- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index cd8cca1..bbf354d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,7 +60,7 @@ Limits that are easy to fix the wrong way: `.fpu softvfp`). It does not spill `d8`–`d15`, and `__ARM_PCS_VFP` is a compile error in that header. `armhf` (`switch_armhf_gcc.S`) is the hard-float port and is what `__ARM_PCS_VFP` selects. It spills - `d8`–`d15` in `stackman_switch`. It does not spill `FPSCR`. Softfp + `d8`–`d15` and the whole `FPSCR` in `stackman_switch`. Softfp does not define `__ARM_PCS_VFP` and still builds as `arm32`. - `aarch64`, `darwin_arm64`, and `win_arm64` do not preserve SME ZA across `stackman_switch`. Do not call `__arm_za_disable`. That helper diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 71ccd0e..033d50f 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -408,7 +408,7 @@ build cannot use that file by mistake. `switch_armhf_gcc.h` is the hard-float port. `platform.h` selects it, and the ABI name `armhf`, when `__ARM_PCS_VFP` is set. `stackman_switch` -spills `d8`–`d15`. `FPSCR` is not spilled. Softfp is not this port: it +spills `d8`–`d15` and the whole `FPSCR`. Softfp is not this port: it does not define `__ARM_PCS_VFP`, and the compiler can still use `d8`–`d15` while building `arm32`. diff --git a/stackman/platforms/switch_armhf_gcc.S b/stackman/platforms/switch_armhf_gcc.S index 99f0325..6e30d3c 100644 --- a/stackman/platforms/switch_armhf_gcc.S +++ b/stackman/platforms/switch_armhf_gcc.S @@ -34,7 +34,11 @@ stackman_switch: mov r3, r0 mov r0, r1 .syntax divided -@ 91 "../platforms/switch_armhf_gcc.h" 1 +@ 98 "../platforms/switch_armhf_gcc.h" 1 + vmrs r2, fpscr + str r2, [sp, #-8]! +@ 0 "" 2 +@ 103 "../platforms/switch_armhf_gcc.h" 1 mov r2, sp @ 0 "" 2 .arm @@ -45,7 +49,7 @@ stackman_switch: blx r3 mov r2, r0 .syntax divided -@ 95 "../platforms/switch_armhf_gcc.h" 1 +@ 107 "../platforms/switch_armhf_gcc.h" 1 mov sp, r0 @ 0 "" 2 .arm @@ -53,6 +57,13 @@ stackman_switch: mov r1, #1 mov r0, r5 blx r4 + .syntax divided +@ 112 "../platforms/switch_armhf_gcc.h" 1 + ldr r2, [sp], #8 + vmsr fpscr, r2 +@ 0 "" 2 + .arm + .syntax unified vldm sp!, {d8-d15} pop {r3, r4, r5, r6, r7, r8, r9, r10, fp, pc} .size stackman_switch, .-stackman_switch @@ -70,10 +81,10 @@ stackman_call: mov r0, r1 mov r3, r2 .syntax divided -@ 111 "../platforms/switch_armhf_gcc.h" 1 +@ 130 "../platforms/switch_armhf_gcc.h" 1 mov r4, sp @ 0 "" 2 -@ 112 "../platforms/switch_armhf_gcc.h" 1 +@ 131 "../platforms/switch_armhf_gcc.h" 1 mov r2, sp @ 0 "" 2 .arm @@ -81,7 +92,7 @@ stackman_call: cmp r3, #0 beq .L4 .syntax divided -@ 116 "../platforms/switch_armhf_gcc.h" 1 +@ 135 "../platforms/switch_armhf_gcc.h" 1 mov sp, r3 @ 0 "" 2 .arm @@ -90,7 +101,7 @@ stackman_call: mov r1, #2 blx r5 .syntax divided -@ 120 "../platforms/switch_armhf_gcc.h" 1 +@ 139 "../platforms/switch_armhf_gcc.h" 1 mov sp, r4 @ 0 "" 2 .arm diff --git a/stackman/platforms/switch_armhf_gcc.h b/stackman/platforms/switch_armhf_gcc.h index 3dac66e..a0156e2 100644 --- a/stackman/platforms/switch_armhf_gcc.h +++ b/stackman/platforms/switch_armhf_gcc.h @@ -5,9 +5,13 @@ * so a copied stack image carries them. stackman_call does not touch * them and does not spill them. * - * FPSCR is not spilled. AAPCS keeps the AHP, DN, FZ, and RMode bits - * across a call. Do not adjust r11: stackman_switch is built with - * omit-frame-pointer, so r11 is not this frame. + * stackman_switch also stores the whole FPSCR in that image and writes + * it back after the restore callback. A thread switch does the same. + * The store includes the caller-saved flag bits. stackman_call does + * not change FPSCR and does not spill it. + * + * Do not adjust r11: stackman_switch is built with omit-frame-pointer, + * so r11 is not this frame. * * Softfp (-mfloat-abi=softfp) does not define __ARM_PCS_VFP, so it * does not use this file. @@ -72,6 +76,14 @@ void *STACKMAN_SWITCH_INASM_NAME(stackman_cb_t callback, void *context) * those, according to abi, that functions must save/restore if they * intend to use them */ + /* Park the whole FPSCR below sp. The eight-byte slot keeps the + * AAPCS alignment, and it is inside the image the callback copies. + * r2 is free until the compiler assigns the saved stack pointer. + */ + __asm__ volatile ( + "vmrs r2, fpscr\n\t" + "str r2, [sp, #-8]!" + ::: "r2", "memory"); /* sp = get stack pointer from assembly code */ __asm__ ("mov %[result], sp" : [result] "=r" (sp)); /* store stack */ @@ -79,6 +91,13 @@ void *STACKMAN_SWITCH_INASM_NAME(stackman_cb_t callback, void *context) /* set stack pointer from sp using assembly */ __asm__ ("mov sp, %[result]" : : [result] "r" (sp)); sp = callback(context, STACKMAN_OP_RESTORE, sp); + /* Reload FPSCR from the stack now current, then release the slot + * before the epilogue pops d8-d15. + */ + __asm__ volatile ( + "ldr r2, [sp], #8\n\t" + "vmsr fpscr, r2" + ::: "r2", "memory"); /* restore registers */ return sp; } From 7da344d18a73c9711a9300db29db28108ba2c2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 17:33:12 +0000 Subject: [PATCH 02/11] Spill FPCR and FPSR in the 64-bit ARM stack switch. A thread switch saves both registers. Linux and macOS stackman_switch store them in a 16-byte slot at the pointer passed to the callback and write them back after the restore callback, before the epilogue pops the callee-saved registers. Windows already stored FPCR and now stores FPSR at [sp, #176]; the frame is 192 bytes. The caller-saved status bits are included. stackman_call does not touch either register. qemu-aarch64 ran a two-stack switch of the Linux assembly. The Darwin object assembled and was not executed. Windows ARM64 was not assembled here. --- AGENTS.md | 5 +++-- docs/abi-register-audit.md | 13 +++++++++--- stackman/platforms/switch_aarch64_gcc.S | 16 +++++++++++++-- stackman/platforms/switch_aarch64_gcc.h | 25 ++++++++++++++++++++++++ stackman/platforms/switch_arm64_msvc.asm | 11 ++++++++--- 5 files changed, 60 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bbf354d..b90e4ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,8 +78,9 @@ Limits that are easy to fix the wrong way: saved value. On RISC-V, `s0` is not a frame chain in `stackman_switch`, and `stackman_call` uses `s0` for the previous stack pointer. Do not adjust it there. -- AAPCS64 FPCR and RISC-V `fcsr` are thread state. They are not per-call - spills. +- `stackman_switch` spills `FPCR` and `FPSR` on `aarch64`, + `darwin_arm64`, and `win_arm64`. RISC-V `fcsr` is still not a + per-call spill. ## Build and test diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 033d50f..facb7b8 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -109,7 +109,10 @@ Two gaps that are not missing base registers: `switch_aarch64_gcc.h`, so regenerating the `.S` from `gen_asm.c` keeps them. The epilogue still reloads the saved `x29`. A callback that relocates a stack still has to rewrite saved frame-pointer slots in the copied bytes. -`__arm_za_disable` is not called. See Roadmap. +`__arm_za_disable` is not called. See Roadmap. `FPCR` and `FPSR` are +stored in a 16-byte slot at the stack pointer passed to the callback +and written back after `STACKMAN_OP_RESTORE`. A thread switch saves +both, and two stacks on one thread would otherwise share them. ### `switch_arm64_msvc.asm` (Windows `win_arm64`) @@ -241,7 +244,9 @@ compiles this file and does not run it, so the `x18` fix needs a Windows ARM64 machine or an emulator before it can be considered tested. **Applied fix.** The `x18` change was made as written above. Unwind codes -were not added. +were not added. `FPSR` is stored at `[sp, #176]` next to the existing +`FPCR` spill. The frame is 192 bytes. `stackman_call` still does not +spill either register. ### AArch64 SysV and Apple: keep `x29` attached to the new stack @@ -542,4 +547,6 @@ first. **Applied fix.** No `fenv` test was added. After the spills above, the original mode should also come back on SysV i386 (x87 control word), -Windows x86 (x87 control word), and Windows x64 (MXCSR). +Windows x86 (x87 control word), Windows x64 (MXCSR), and the 64-bit ARM +ABIs (`FPCR`, with `FPSR` restored alongside it). RISC-V `fcsr` is still +not spilled. diff --git a/stackman/platforms/switch_aarch64_gcc.S b/stackman/platforms/switch_aarch64_gcc.S index d2b9120..2b89cda 100644 --- a/stackman/platforms/switch_aarch64_gcc.S +++ b/stackman/platforms/switch_aarch64_gcc.S @@ -79,7 +79,12 @@ LABEL(stackman_switch) mov x3, x0 mov x0, x1 #APP -// 59 "../platforms/switch_aarch64_gcc.h" 1 +// 70 "../platforms/switch_aarch64_gcc.h" 1 + mrs x1, fpcr + mrs x2, fpsr + stp x1, x2, [sp, #-16]! +// 0 "" 2 +// 76 "../platforms/switch_aarch64_gcc.h" 1 mov x2, sp // 0 "" 2 #NO_APP @@ -89,7 +94,7 @@ LABEL(stackman_switch) blr x3 mov x2, x0 #APP -// 67 "../platforms/switch_aarch64_gcc.h" 1 +// 84 "../platforms/switch_aarch64_gcc.h" 1 sub x1, sp, x0 mov sp, x0 sub x29, x29, x1 @@ -98,6 +103,13 @@ LABEL(stackman_switch) mov w1, 1 mov x0, x20 blr x19 +#APP +// 93 "../platforms/switch_aarch64_gcc.h" 1 + ldp x1, x2, [sp], #16 + msr fpcr, x1 + msr fpsr, x2 +// 0 "" 2 +#NO_APP ldp x19, x20, [sp, 16] ldp x21, x22, [sp, 32] ldp x23, x24, [sp, 48] diff --git a/stackman/platforms/switch_aarch64_gcc.h b/stackman/platforms/switch_aarch64_gcc.h index 8e7c15c..87a8c86 100644 --- a/stackman/platforms/switch_aarch64_gcc.h +++ b/stackman/platforms/switch_aarch64_gcc.h @@ -3,6 +3,11 @@ * and additional information for gcc inline arm assembly: * http://www.ethernut.de/en/documents/arm-inline-asm.html * + * stackman_switch stores FPCR and FPSR in the stack image and writes + * them back after the restore callback. A thread switch saves both, + * including the caller-saved status bits. stackman_call does not + * change them and does not spill them. + * */ /* clang cannot perform inline assembly using specific __attr__ * instructions, and so it may use a base pointer and other @@ -55,6 +60,18 @@ void *STACKMAN_SWITCH_INASM_NAME(stackman_cb_t callback, void *context) #ifdef FP_PRESERVE __asm__ volatile ("" : : : FP_PRESERVE); #endif + /* FPCR and FPSR are one thread context on Linux and macOS. + * Park both in a 16-byte slot and pass that sp to the callback, + * so the copied image carries them. Sixteen bytes keeps the + * AAPCS64 alignment. x1 and x2 are free until the compiler + * assigns the saved stack pointer. The reload clobbers the + * same registers after the restore callback returns. + */ + __asm__ volatile ( + "mrs x1, fpcr\n\t" + "mrs x2, fpsr\n\t" + "stp x1, x2, [sp, #-16]!" + ::: "x1", "x2", "memory"); /* sp = get stack pointer from assembly code */ __asm__ ("mov %[result], sp" : [result] "=r" (sp)); /* store stack */ @@ -70,6 +87,14 @@ void *STACKMAN_SWITCH_INASM_NAME(stackman_cb_t callback, void *context) "sub x29, x29, x1" : : [result] "r" (sp) : "x1"); sp = callback(context, STACKMAN_OP_RESTORE, sp); + /* Reload FPCR and FPSR from the stack now current, then release + * the slot before the epilogue pops the callee-saved registers. + */ + __asm__ volatile ( + "ldp x1, x2, [sp], #16\n\t" + "msr fpcr, x1\n\t" + "msr fpsr, x2" + ::: "x1", "x2", "memory"); return sp; } diff --git a/stackman/platforms/switch_arm64_msvc.asm b/stackman/platforms/switch_arm64_msvc.asm index 0745540..9190447 100644 --- a/stackman/platforms/switch_arm64_msvc.asm +++ b/stackman/platforms/switch_arm64_msvc.asm @@ -12,12 +12,15 @@ ; args "callback", "context" are in x0, x1 ; push non-volatile x18-x28 registers, in addition to fp,rl pair. ; x29 is fp, x30 is lr. - stp x29, x30, [sp, #-176]! ; store fp,lr pair, allocate stack space + stp x29, x30, [sp, #-192]! ; store fp,lr pair, allocate stack space mov x29, sp ; and set fp pointing to old fp on stack ; push non-volatile registers 18-x28 and fp regs d8-d15) - ; also fpcr (fp control register) + ; also fpcr and fpsr. A thread switch saves both. The status + ; bits in fpsr are caller-saved for a normal call. mrs x2,fpcr + mrs x3,fpsr stp x2, x18, [sp, #16] + str x3, [sp, #176] stp x19, x20, [sp, #32] stp x21, x22, [sp, #48] stp x23, x24, [sp, #64] @@ -62,9 +65,11 @@ ldp d10, d11, [sp, #128] ldp d12, d13, [sp, #144] ldp d14, d15, [sp, #160] + ldr x3, [sp, #176] msr fpcr, x2 + msr fpsr, x3 ;return - ldp x29, x30, [sp], #176 + ldp x29, x30, [sp], #192 ret ENDP From 021949cd4f1b289711488e3a46e021e432afa72e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 17:48:24 +0000 Subject: [PATCH 03/11] Note that softfp is not a supported platform. A softfp build does not define __ARM_PCS_VFP, so it selects arm32 and runs. The soft-float assembly does not preserve FPSCR. Do not add that spill to switch_arm_gcc.S: the file is softvfp, and vmrs fpscr is an undefined instruction without VFP. --- AGENTS.md | 4 +++- README.md | 5 +++++ docs/abi-register-audit.md | 11 ++++++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b90e4ba..d4aaca6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,7 +61,9 @@ Limits that are easy to fix the wrong way: a compile error in that header. `armhf` (`switch_armhf_gcc.S`) is the hard-float port and is what `__ARM_PCS_VFP` selects. It spills `d8`–`d15` and the whole `FPSCR` in `stackman_switch`. Softfp - does not define `__ARM_PCS_VFP` and still builds as `arm32`. + (`-mfloat-abi=softfp`) is not a supported platform. It still builds + as `arm32` and runs, and that assembly does not preserve `FPSCR`. + Do not add an `FPSCR` spill to `switch_arm_gcc.S` to cover it. - `aarch64`, `darwin_arm64`, and `win_arm64` do not preserve SME ZA across `stackman_switch`. Do not call `__arm_za_disable`. That helper commits the outgoing caller's ZA and turns ZA off. Nothing in the diff --git a/README.md b/README.md index 7ac261a..47fa55b 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,11 @@ calling convention, plus archive format: The 64-bit ARM ABIs (`aarch64`, `darwin_arm64`, and `win_arm64`) do not preserve SME ZA context across `stackman_switch`. +Softfp (`-mfloat-abi=softfp`) is not a supported platform, and it is not +clear how widely it is used. In practice that build does not define +`__ARM_PCS_VFP`, so it selects `arm32` and runs. The soft-float assembly +does not preserve `FPSCR`. + All platforms are automatically built and tested by GitHub Actions CI on every commit. The BSDs share the `sysv_amd64` ABI with Linux and therefore need no separate diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index facb7b8..319c6ae 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -413,9 +413,14 @@ build cannot use that file by mistake. `switch_armhf_gcc.h` is the hard-float port. `platform.h` selects it, and the ABI name `armhf`, when `__ARM_PCS_VFP` is set. `stackman_switch` -spills `d8`–`d15` and the whole `FPSCR`. Softfp is not this port: it -does not define `__ARM_PCS_VFP`, and the compiler can still use -`d8`–`d15` while building `arm32`. +spills `d8`–`d15` and the whole `FPSCR`. + +Softfp (`-mfloat-abi=softfp`) is not a supported platform, and it is not +clear how widely it is used. It does not define `__ARM_PCS_VFP`, so the +build selects `arm32` and runs. That assembly does not preserve `FPSCR`. +The compiler can still use `d8`–`d15` while building `arm32`. Do not add +an `FPSCR` spill to the soft-float file to cover softfp: that file is +`.fpu softvfp`, and `vmrs fpscr` is an undefined instruction without VFP. ### AArch64 SME lazy save From 8c4ddd7974c6f0547a2fee3ed540fd4706e2d0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 18:03:43 +0000 Subject: [PATCH 04/11] Spill fcsr in the RISC-V stack switch. A thread switch saves the whole fcsr. stackman_switch stores it in the existing 8-byte pad at the pointer passed to the callback and writes it back after the restore callback, before the epilogue pops fs0-fs11. The caller-saved exception flags are included. stackman_call does not touch fcsr. qemu-riscv64 ran a two-stack switch of this assembly. --- AGENTS.md | 5 +++-- docs/abi-register-audit.md | 14 ++++++++------ stackman/platforms/switch_riscv64_gcc.S | 21 ++++++++++++++++++--- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d4aaca6..b71d028 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,8 +81,9 @@ Limits that are easy to fix the wrong way: `stackman_switch`, and `stackman_call` uses `s0` for the previous stack pointer. Do not adjust it there. - `stackman_switch` spills `FPCR` and `FPSR` on `aarch64`, - `darwin_arm64`, and `win_arm64`. RISC-V `fcsr` is still not a - per-call spill. + `darwin_arm64`, and `win_arm64`, and spills `fcsr` on `riscv64`. + The RISC-V slot is the existing 8-byte pad at the published stack + pointer. `stackman_call` does not touch these registers. ## Build and test diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 319c6ae..17c1f31 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -196,9 +196,12 @@ file and does not run it. the `_M_ARM` branch in `platform.h`, the Debug|ARM configurations in vs2017, vs2019, and vs2022, and `lib/win_arm/stackman.lib`. The alignment alternative was not used. Windows ARM64 stays. -- The RISC-V register spill was already complete. The optional `s0` frame - delta was left unchanged: `s0` is not a frame chain in `stackman_switch`, - and `stackman_call` uses it to hold the previous stack pointer. +- The RISC-V integer and `fs0`–`fs11` spill was already complete. The + optional `s0` frame delta was left unchanged: `s0` is not a frame chain + in `stackman_switch`, and `stackman_call` uses it to hold the previous + stack pointer. `fcsr` is stored at `0(sp)`, the pad below `fs11`, and + written back after `STACKMAN_OP_RESTORE`. A thread switch saves the + whole register. `stackman_call` still does not spill it. ### RISC-V `stackman_call` @@ -552,6 +555,5 @@ first. **Applied fix.** No `fenv` test was added. After the spills above, the original mode should also come back on SysV i386 (x87 control word), -Windows x86 (x87 control word), Windows x64 (MXCSR), and the 64-bit ARM -ABIs (`FPCR`, with `FPSR` restored alongside it). RISC-V `fcsr` is still -not spilled. +Windows x86 (x87 control word), Windows x64 (MXCSR), the 64-bit ARM +ABIs (`FPCR`, with `FPSR` restored alongside it), and RISC-V (`fcsr`). diff --git a/stackman/platforms/switch_riscv64_gcc.S b/stackman/platforms/switch_riscv64_gcc.S index 3a504ec..7ae1ee6 100644 --- a/stackman/platforms/switch_riscv64_gcc.S +++ b/stackman/platforms/switch_riscv64_gcc.S @@ -46,9 +46,12 @@ LABEL(stackman_switch) .LFB0: CFI_STARTPROC - /* Allocate stack frame (160 bytes) - * Layout: ra(8) + s0-s11(12*8=96) + fs0-fs11(12*8=96) = 200 bytes - * Round to 208 for 16-byte alignment + /* 208 bytes, 16-byte aligned. + * ra, s0-s11, and fs0-fs11 occupy bytes 8 through 207. + * The pad at byte 0 holds fcsr, so the pointer passed to the + * callback includes it. A thread switch saves the whole register + * (rounding mode and accrued exception flags). stackman_call does + * not touch fcsr. */ addi sp, sp, -208 CFI_DEF_CFA_OFFSET(208) @@ -94,6 +97,12 @@ LABEL(stackman_switch) fsd fs9, 24(sp) fsd fs10, 16(sp) fsd fs11, 8(sp) + + /* frcsr after the first FP spill, so a lazy FP enable has run. + * t0 is free. The eight-byte pad keeps the 16-byte alignment. + */ + frcsr t0 + sd t0, 0(sp) /* Preserve callback and context in callee-saved registers */ mv s2, a0 /* s2 = callback */ @@ -114,6 +123,12 @@ LABEL(stackman_switch) li a1, 1 /* a1 = STACKMAN_OP_RESTORE */ mv a2, sp /* a2 = new sp */ jalr s2 /* call callback */ + + /* Reload fcsr from the stack now current, before the epilogue + * pops fs0-fs11. a0 is the callback result and stays there. + */ + ld t0, 0(sp) + fscsr t0 /* Restore callee-saved floating point registers */ fld fs11, 8(sp) From 4cf9e3ec7f57e0ffa33d2ffd958d77cfaf0e06a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 20:38:02 +0000 Subject: [PATCH 05/11] Record the armhf FPSCR spill in the earlier audit notes. The squash of the armhf port said FPSCR was still not spilled. This branch stores the whole register, so those applied-fix sentences now say so. --- docs/abi-register-audit.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 17c1f31..acbff8d 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -190,8 +190,8 @@ file and does not run it. AArch64 still adjusts `x29`, which the prologue sets with `mov x29, sp`. - `__ARM_PCS_VFP` selects `armhf` (`switch_armhf_gcc.S`), which spills - `d8`–`d15`. `FPSCR` is still not spilled. The soft-float file was - left unchanged. + `d8`–`d15` and the whole `FPSCR`. The soft-float file was left + unchanged. - Windows ARM32 was removed: `switch_arm_msvc.asm`, `switch_arm_msvc.h`, the `_M_ARM` branch in `platform.h`, the Debug|ARM configurations in vs2017, vs2019, and vs2022, and `lib/win_arm/stackman.lib`. The @@ -368,7 +368,7 @@ The shipped integer set (`r4`–`r11`, `lr`) needs no new stores. commented as inline-only. The optional `r11` adjustment was applied and then removed. See the frame-pointer applied fix above. `d8`–`d15` were not added to this file. `__ARM_PCS_VFP` selects `armhf` -(`switch_armhf_gcc.S` spills `d8`–`d15`; `FPSCR` is still not spilled). +(`switch_armhf_gcc.S` spills `d8`–`d15` and the whole `FPSCR`). ### Windows ARM32: delete it, or align `sp` before `vpush` From b184a2e2075acc533aa6ef2e04ba9d11cab6b939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 20:46:09 +0000 Subject: [PATCH 06/11] Keep the register audit as a historic record. Later ports were being written into docs/abi-register-audit.md. That file is restored to the audit as it was merged. Current ABI, switching, and spill rules for each platform are in docs/platform-abi.md. --- AGENTS.md | 4 +- README.md | 3 + docs/abi-register-audit.md | 52 ++++-------- docs/platform-abi.md | 157 +++++++++++++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 36 deletions(-) create mode 100644 docs/platform-abi.md diff --git a/AGENTS.md b/AGENTS.md index b71d028..3b0616d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,7 +53,9 @@ those comments unless you are regenerating the file. Volatile registers may change. Do not add a test that treats them as preserved. -Details and the original audit are in `docs/abi-register-audit.md`. +Current spills are in `docs/platform-abi.md`. +`docs/abi-register-audit.md` is the audit from when those gaps were +found and fixed. Do not edit the audit for a later port or spill. Limits that are easy to fix the wrong way: - `arm32` is the soft-float assembly (`switch_arm_gcc.S`, `.arch armv5t`, diff --git a/README.md b/README.md index 47fa55b..2d74611 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,9 @@ calling convention, plus archive format: - **FreeBSD, OpenBSD, NetBSD (System V ABI)** - sysv_amd64 (64-bit x86_64) +Which registers each ABI spills is listed in +[`docs/platform-abi.md`](docs/platform-abi.md). + The 64-bit ARM ABIs (`aarch64`, `darwin_arm64`, and `win_arm64`) do not preserve SME ZA context across `stackman_switch`. diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index acbff8d..8268fad 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -109,10 +109,7 @@ Two gaps that are not missing base registers: `switch_aarch64_gcc.h`, so regenerating the `.S` from `gen_asm.c` keeps them. The epilogue still reloads the saved `x29`. A callback that relocates a stack still has to rewrite saved frame-pointer slots in the copied bytes. -`__arm_za_disable` is not called. See Roadmap. `FPCR` and `FPSR` are -stored in a 16-byte slot at the stack pointer passed to the callback -and written back after `STACKMAN_OP_RESTORE`. A thread switch saves -both, and two stacks on one thread would otherwise share them. +`__arm_za_disable` is not called. See Roadmap. ### `switch_arm64_msvc.asm` (Windows `win_arm64`) @@ -161,7 +158,7 @@ file and does not run it. | `switch_x64_msvc.asm` | `rbx`, `rbp`, `rdi`, `rsi`, `r12`–`r15`, `xmm6`–`xmm15` | Those registers match the Windows x64 ABI. MXCSR[6:15] is non-volatile on Windows and is not spilled, so a changed rounding mode does not travel with the stack. The SysV file does save MXCSR. | | `switch_x86_gcc.S` | `ebx`, `esi`, `edi`, `ebp` | Integer set is complete. There is no callee-saved XMM on i386 SysV. The x87 control word is callee-saved in that ABI and is not saved. | | `switch_x86_msvc.asm` | `ebx`, `esi`, `edi`, `ebp`, plus the C wrapper saves `fs:[0]` | Integer set is complete for cdecl. Same x87 control-word gap. | -| `switch_arm_gcc.S` | `r4`–`r11`, `lr` | Complete for the soft-float `arm-linux-gnueabi` ABI this tree ships (`.arch armv5t`, `.fpu softvfp`). `d8`–`d15` are absent. The inline header clobbers them, so a hard-float or softfp build of this `.S` would drop callee-saved VFP state. The live frame pointer is not adjusted. | +| `switch_arm_gcc.S` | `r4`–`r11`, `lr` | Complete for the soft-float `arm-linux-gnueabi` ABI this tree ships (`.arch armv5t`, `.fpu softvfp`). `d8`–`d15` are absent. The inline header clobbers them, so a hard-float or softfp build of this `.S` would drop callee-saved VFP state. `armhf` is not a configured ABI. The live frame pointer is not adjusted. | | `switch_arm_msvc.asm` | `r4`–`r11`, `lr`, `q4`–`q7` (`d8`–`d15`), `FPSCR` | The set matches 32-bit Windows ARM. Alignment does not. `push {r4-r10, fp, lr}` is 9 words, so `sp` is 4-byte aligned for `vpush {q4-q7}`. A doubleword `VPUSH` requires 8-byte alignment. `stackman_call` pushes 3 registers and then `blx`s with `sp` still 4 mod 8. The changelog records this ABI as dropped; the file is still wrong if it is built. | | `switch_riscv64_gcc.S` | `ra`, `s0`–`s11`, `fs0`–`fs11` | `stackman_switch` is complete for LP64D. `gp` and `tp` are left alone. `fcsr` is thread `fenv` state, not a per-call spill. | @@ -189,19 +186,13 @@ file and does not run it. keep the callback or the new stack pointer in `r11` across that asm. AArch64 still adjusts `x29`, which the prologue sets with `mov x29, sp`. -- `__ARM_PCS_VFP` selects `armhf` (`switch_armhf_gcc.S`), which spills - `d8`–`d15` and the whole `FPSCR`. The soft-float file was left - unchanged. - Windows ARM32 was removed: `switch_arm_msvc.asm`, `switch_arm_msvc.h`, the `_M_ARM` branch in `platform.h`, the Debug|ARM configurations in vs2017, vs2019, and vs2022, and `lib/win_arm/stackman.lib`. The alignment alternative was not used. Windows ARM64 stays. -- The RISC-V integer and `fs0`–`fs11` spill was already complete. The - optional `s0` frame delta was left unchanged: `s0` is not a frame chain - in `stackman_switch`, and `stackman_call` uses it to hold the previous - stack pointer. `fcsr` is stored at `0(sp)`, the pad below `fs11`, and - written back after `STACKMAN_OP_RESTORE`. A thread switch saves the - whole register. `stackman_call` still does not spill it. +- The RISC-V register spill was already complete. The optional `s0` frame + delta was left unchanged: `s0` is not a frame chain in `stackman_switch`, + and `stackman_call` uses it to hold the previous stack pointer. ### RISC-V `stackman_call` @@ -247,9 +238,7 @@ compiles this file and does not run it, so the `x18` fix needs a Windows ARM64 machine or an emulator before it can be considered tested. **Applied fix.** The `x18` change was made as written above. Unwind codes -were not added. `FPSR` is stored at `[sp, #176]` next to the existing -`FPCR` spill. The frame is 192 bytes. `stackman_call` still does not -spill either register. +were not added. ### AArch64 SysV and Apple: keep `x29` attached to the new stack @@ -367,8 +356,7 @@ The shipped integer set (`r4`–`r11`, `lr`) needs no new stores. **Applied fix.** `__ARM_PCS_VFP` is a compile error, and `CP_REGISTERS` is commented as inline-only. The optional `r11` adjustment was applied and then removed. See the frame-pointer applied fix above. `d8`–`d15` were -not added to this file. `__ARM_PCS_VFP` selects `armhf` -(`switch_armhf_gcc.S` spills `d8`–`d15` and the whole `FPSCR`). +not added. A separate hard-float port is on the roadmap. ### Windows ARM32: delete it, or align `sp` before `vpush` @@ -405,25 +393,20 @@ alternative was not used. ## Roadmap -SME lazy save is still open. The ARM32 hard-float port is `armhf`. +Two items from the audit are still open. ### ARM32 hard-float -The soft-float file `switch_arm_gcc.S` is unchanged (`arm-linux-gnueabi`, +The shipped `switch_arm_gcc.S` is soft-float (`arm-linux-gnueabi`, `.arch armv5t`, `.fpu softvfp`) and does not spill `d8`–`d15`. -`__ARM_PCS_VFP` is still a compile error in that header, so a hard-float -build cannot use that file by mistake. +`__ARM_PCS_VFP` is a compile error, so a hard-float build cannot use that +file by mistake. -`switch_armhf_gcc.h` is the hard-float port. `platform.h` selects it, -and the ABI name `armhf`, when `__ARM_PCS_VFP` is set. `stackman_switch` -spills `d8`–`d15` and the whole `FPSCR`. - -Softfp (`-mfloat-abi=softfp`) is not a supported platform, and it is not -clear how widely it is used. It does not define `__ARM_PCS_VFP`, so the -build selects `arm32` and runs. That assembly does not preserve `FPSCR`. -The compiler can still use `d8`–`d15` while building `arm32`. Do not add -an `FPSCR` spill to the soft-float file to cover softfp: that file is -`.fpu softvfp`, and `vmrs fpscr` is an undefined instruction without VFP. +A hard-float port is a new assembly, alongside the soft-float file. It +spills `d8`–`d15` in `stackman_switch`, has its own ABI name, and is what +a `__ARM_PCS_VFP` build selects. The `#error` stays until that port +exists. Softfp is not that port: it does not define `__ARM_PCS_VFP`, and +the compiler can still use `d8`–`d15`. ### AArch64 SME lazy save @@ -555,5 +538,4 @@ first. **Applied fix.** No `fenv` test was added. After the spills above, the original mode should also come back on SysV i386 (x87 control word), -Windows x86 (x87 control word), Windows x64 (MXCSR), the 64-bit ARM -ABIs (`FPCR`, with `FPSR` restored alongside it), and RISC-V (`fcsr`). +Windows x86 (x87 control word), and Windows x64 (MXCSR). diff --git a/docs/platform-abi.md b/docs/platform-abi.md new file mode 100644 index 0000000..58a2f79 --- /dev/null +++ b/docs/platform-abi.md @@ -0,0 +1,157 @@ +# Platform ABI and register spills + +Current contract for `stackman_switch` and `stackman_call`. The assembly +and inline headers are the source of truth. This file is the map. + +`docs/abi-register-audit.md` is the audit from when the gaps were found +and fixed. Do not add later ports or spills to that file. + +## Switching + +`stackman_switch` may resume a different caller. It spills the +non-volatile set for that ABI, calls the callback with +`STACKMAN_OP_SAVE`, installs the stack pointer that callback returns, +calls it again with `STACKMAN_OP_RESTORE`, restores the saved state, +and returns the second result. + +`stackman_call` returns to the same caller. A null stack is left +unchanged. The pointer passed to the callback is the previous stack +pointer. The routine preserves only registers it modifies. + +Volatile registers may change across either call. + +A floating-point control or status register is spilled in +`stackman_switch` when a thread switch saves it. The whole register is +stored, including caller-saved flag bits. The store is in the frame +before that stack pointer is published to the callback. The reload is +after `STACKMAN_OP_RESTORE` and before the epilogue pops callee-saved +registers. `stackman_call` does not touch these registers. + +`STACKMAN_SP_ALIGN` follows `STACKMAN_STACK_ALIGN` below. + +## Platforms + +`platform.h` selects the source. `win_aarch64` is accepted and +normalized to `win_arm64`. FreeBSD, OpenBSD, and NetBSD use the +`sysv_amd64` sources. Windows ARM32 (`win_arm`) was removed in 1.0.0. + +| ABI | Source | `STACKMAN_STACK_ALIGN` | +| --- | --- | --- | +| `sysv_i386` | `switch_x86_gcc` | 16 | +| `sysv_amd64`, `darwin_x86_64` | `switch_x86_64_gcc` | 16 | +| `arm32` | `switch_arm_gcc` | 8 | +| `armhf` | `switch_armhf_gcc` | 8 | +| `aarch64`, `darwin_arm64` | `switch_aarch64_gcc` | 16 | +| `riscv64` | `switch_riscv64_gcc` | 16 | +| `win_x86` | `switch_x86_msvc` | 4 | +| `win_x64` | `switch_x64_msvc` | 16 | +| `win_arm64` | `switch_arm64_msvc` | 16 | + +### `sysv_i386` + +`stackman_switch` spills `ebx`, `esi`, `edi`, `ebp`, and the x87 +control word (`fstcw` / `fldcw`). It does not spill MXCSR. There is no +callee-saved XMM register in this ABI. + +`stackman_call` does not spill the x87 control word. + +### `sysv_amd64` and `darwin_x86_64` + +`stackman_switch` spills `rbx`, `rbp`, `r12`–`r15`, the x87 control +word (`fstcw` / `fldcw`), and MXCSR (`stmxcsr` / `ldmxcsr`). XMM +registers are volatile. `rbp` is adjusted by the stack delta when the +callback installs a new stack. + +`stackman_call` does not spill the x87 control word or MXCSR. + +### `arm32` + +Soft-float AAPCS, `arm-linux-gnueabi`. The assembly is `.arch armv5t` +and `.fpu softvfp`. `__ARM_PCS_VFP` is a compile error in this header. + +`stackman_switch` spills `r4`–`r11` and `lr`. It does not spill +`d8`–`d15` or `FPSCR`. `r11` is not a frame chain here: +`omit-frame-pointer` is set, the prologue pushes `r11` without writing +it, and the epilogue pops the saved value. Do not adjust `r11`. + +`stackman_call` does not spill `d8`–`d15` or `FPSCR`. + +Softfp (`-mfloat-abi=softfp`) is not a supported platform. It does not +define `__ARM_PCS_VFP`, so it selects `arm32` and runs, and this +assembly does not preserve `FPSCR`. Do not add `vmrs fpscr` to +`switch_arm_gcc.S`. That instruction is undefined without VFP. The +compiler can still use `d8`–`d15` while building `arm32`. + +### `armhf` + +Hard-float AAPCS-VFP, `arm-linux-gnueabihf`. `__ARM_PCS_VFP` selects +this ABI. It does not link with `arm32`. + +`stackman_switch` spills `r4`–`r11`, `lr`, `d8`–`d15`, and the whole +`FPSCR`. `FPSCR` sits in an 8-byte slot below the pointer passed to the +callback, and is written back before `d8`–`d15` are popped. `r3` in the +integer push is padding. `r11` is not adjusted, for the same reason as +`arm32`. + +`stackman_call` does not spill `d8`–`d15` or `FPSCR`. + +### `aarch64` and `darwin_arm64` + +`stackman_switch` spills `x19`–`x28`, `x29`, `x30`, and the low 64 bits +of `v8`–`v15` (`d8`–`d15`). The high 64 bits of `v8`–`v15` are +caller-saved and are not spilled. `x18` is left alone. + +`FPCR` and `FPSR` are stored in a 16-byte slot at the pointer passed to +the callback and written back after `STACKMAN_OP_RESTORE`. `x29` is +adjusted by the stack delta. The prologue sets `x29` from `sp`, so the +delta is computed from the lowered stack pointer. Do not list `x29` as +an asm clobber. + +`stackman_call` does not spill `FPCR`, `FPSR`, or `d8`–`d15`. + +SME ZA is not preserved. Do not call `__arm_za_disable`. + +### `win_arm64` + +`stackman_switch` spills `x18`–`x28`, `x29`, `x30`, `d8`–`d15`, `FPCR`, +and `FPSR`. The frame is 192 bytes. `FPSR` is at `[sp, #176]`, next to +the `FPCR` spill. `x18` is the TEB for the whole time user code runs: +it is stored and reloaded, and the live register is never written. The +frame pointer is adjusted by the stack delta. No `.seh_*` unwind codes +are emitted. The high 64 bits of `v8`–`v15` are not spilled. + +`stackman_call` uses a 32-byte frame. It does not touch `x18`, `FPCR`, +or `FPSR`. + +SME ZA is not preserved. Do not call the AAPCS64 `__arm_za_disable` +helper on Windows. + +### `riscv64` + +LP64D. `stackman_switch` spills `ra`, `s0`–`s11`, `fs0`–`fs11`, and the +whole `fcsr` (`frm` and the accrued exception flags). The frame is 208 +bytes. `fcsr` uses the existing 8-byte pad at the published stack +pointer, below `fs11`. `gp` and `tp` are left alone. `s0` is not a frame +chain in `stackman_switch`. Do not adjust it. + +`stackman_call` spills `ra` and `s0`. `s0` holds the previous stack +pointer. A null stack is left unchanged. It does not spill `fcsr`. + +Vector CSRs (`vcsr`, `vl`, `vtype`, `vxrm`, `vxsat`) are not spilled. + +### `win_x86` + +`stackman_switch_raw` spills `ebx`, `esi`, `edi`, `ebp`, and the x87 +control word (`fstcw` / `fldcw`). The C wrapper around it saves and +restores `fs:[0]`, the SEH chain. MXCSR is not spilled. + +`stackman_call` does not spill the x87 control word. The SEH save is +only on `stackman_switch`. + +### `win_x64` + +`stackman_switch` spills `rbx`, `rbp`, `rdi`, `rsi`, `r12`–`r15`, +`xmm6`–`xmm15`, and MXCSR. MXCSR sits in the 8-byte pad at the end of +the 168-byte XMM area. + +`stackman_call` does not spill MXCSR or `xmm6`–`xmm15`. From ea6e45ce213865b8717d2a9498e0944819933a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Thu, 15 Oct 2026 11:37:51 +0000 Subject: [PATCH 07/11] Note that the register audit is the historical record of PR 21. Later ports stay in docs/platform-abi.md. --- docs/abi-register-audit.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 8268fad..45bf3fe 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -1,5 +1,11 @@ # ABI register audit +This is the historical record of the register audit in +[pull request 21](https://github.com/stackless-dev/stackman/pull/21). +It describes the gaps found then and the fixes applied in that change. +Later ports and spills belong in +[`platform-abi.md`](platform-abi.md), not here. + Audit of the assembly that implements `stackman_switch` and `stackman_call`. The question is whether each routine spills every non-volatile register to the stack before calling out, and loads that image back before returning. From 0e1a180562f94bc07235321581cf9823aa8d5c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Thu, 15 Oct 2026 12:14:43 +0000 Subject: [PATCH 08/11] Correct the AArch64 unwind info for the FPCR slot. The 16-byte spill lowered sp without telling the CFI, so callee-saved slots were described 16 bytes low across both callbacks. The CFA is sp+176 until the slot is popped. The macros sit outside the shared asm block, so the Darwin assembly stays empty there. The armhf note says the published pointer is the FPSCR slot. The Windows ARM64 note puts FPSR at [sp, #176] and says the epilogue reloads x18. The audit banner says the body is the PR 21 outcome and is not the current tree. --- docs/abi-register-audit.md | 3 ++- docs/platform-abi.md | 20 +++++++++++--------- stackman/platforms/switch_aarch64_gcc.S | 13 ++++++++++--- stackman/platforms/switch_aarch64_gcc.h | 12 ++++++++++++ 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 45bf3fe..c8381b2 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -2,7 +2,8 @@ This is the historical record of the register audit in [pull request 21](https://github.com/stackless-dev/stackman/pull/21). -It describes the gaps found then and the fixes applied in that change. +The text below is that pull request's outcome, including gaps a later +change has closed. It is not a description of the current tree. Later ports and spills belong in [`platform-abi.md`](platform-abi.md), not here. diff --git a/docs/platform-abi.md b/docs/platform-abi.md index 58a2f79..94cdae5 100644 --- a/docs/platform-abi.md +++ b/docs/platform-abi.md @@ -88,10 +88,10 @@ Hard-float AAPCS-VFP, `arm-linux-gnueabihf`. `__ARM_PCS_VFP` selects this ABI. It does not link with `arm32`. `stackman_switch` spills `r4`–`r11`, `lr`, `d8`–`d15`, and the whole -`FPSCR`. `FPSCR` sits in an 8-byte slot below the pointer passed to the -callback, and is written back before `d8`–`d15` are popped. `r3` in the -integer push is padding. `r11` is not adjusted, for the same reason as -`arm32`. +`FPSCR`. The published stack pointer is the 8-byte `FPSCR` slot, below +the `d8`–`d15` spill. That slot is written back before those registers +are popped. `r3` in the integer push is padding. `r11` is not adjusted, +for the same reason as `arm32`. `stackman_call` does not spill `d8`–`d15` or `FPSCR`. @@ -114,11 +114,13 @@ SME ZA is not preserved. Do not call `__arm_za_disable`. ### `win_arm64` `stackman_switch` spills `x18`–`x28`, `x29`, `x30`, `d8`–`d15`, `FPCR`, -and `FPSR`. The frame is 192 bytes. `FPSR` is at `[sp, #176]`, next to -the `FPCR` spill. `x18` is the TEB for the whole time user code runs: -it is stored and reloaded, and the live register is never written. The -frame pointer is adjusted by the stack delta. No `.seh_*` unwind codes -are emitted. The high 64 bits of `v8`–`v15` are not spilled. +and `FPSR`. The frame is 192 bytes. `FPCR` is stored with `x18` at +`[sp, #16]`. `FPSR` is the 8-byte slot at `[sp, #176]`, next to `d15`, +with 8 bytes of padding at `[sp, #184]`. The body does not use `x18` +as a scratch. The epilogue reloads the spilled value. `x18` is the TEB +for the whole time user code runs. The frame pointer is adjusted by +the stack delta. No `.seh_*` unwind codes are emitted. The high 64 +bits of `v8`–`v15` are not spilled. `stackman_call` uses a 32-byte frame. It does not touch `x18`, `FPCR`, or `FPSR`. diff --git a/stackman/platforms/switch_aarch64_gcc.S b/stackman/platforms/switch_aarch64_gcc.S index 2b89cda..208610d 100644 --- a/stackman/platforms/switch_aarch64_gcc.S +++ b/stackman/platforms/switch_aarch64_gcc.S @@ -84,17 +84,23 @@ LABEL(stackman_switch) mrs x2, fpsr stp x1, x2, [sp, #-16]! // 0 "" 2 -// 76 "../platforms/switch_aarch64_gcc.h" 1 +// 85 "../platforms/switch_aarch64_gcc.h" 1 mov x2, sp // 0 "" 2 #NO_APP + /* The 16-byte FPCR/FPSR slot is outside the compiler frame. + * Keep it out of the #APP block: Darwin assembles that block, + * and these macros are empty there. A gcc -S regen does not + * emit this, because the compiler does not see the sp write. + */ + CFI_DEF_CFA_OFFSET(176) mov w1, 0 mov x20, x0 mov x19, x3 blr x3 mov x2, x0 #APP -// 84 "../platforms/switch_aarch64_gcc.h" 1 +// 93 "../platforms/switch_aarch64_gcc.h" 1 sub x1, sp, x0 mov sp, x0 sub x29, x29, x1 @@ -104,12 +110,13 @@ LABEL(stackman_switch) mov x0, x20 blr x19 #APP -// 93 "../platforms/switch_aarch64_gcc.h" 1 +// 102 "../platforms/switch_aarch64_gcc.h" 1 ldp x1, x2, [sp], #16 msr fpcr, x1 msr fpsr, x2 // 0 "" 2 #NO_APP + CFI_DEF_CFA_OFFSET(160) ldp x19, x20, [sp, 16] ldp x21, x22, [sp, 32] ldp x23, x24, [sp, 48] diff --git a/stackman/platforms/switch_aarch64_gcc.h b/stackman/platforms/switch_aarch64_gcc.h index 87a8c86..9058c28 100644 --- a/stackman/platforms/switch_aarch64_gcc.h +++ b/stackman/platforms/switch_aarch64_gcc.h @@ -72,6 +72,15 @@ void *STACKMAN_SWITCH_INASM_NAME(stackman_cb_t callback, void *context) "mrs x2, fpsr\n\t" "stp x1, x2, [sp, #-16]!" ::: "x1", "x2", "memory"); + /* The compiler's CFI still describes its own frame. This + * adjusts by the slot just pushed. It is not in the stp asm: + * that string is copied into the shared .S, and Darwin + * assembles the copy with no .cfi_startproc. The .S sets the + * CFA with CFI_DEF_CFA_OFFSET instead. + */ +#if !defined(__APPLE__) + __asm__ volatile (".cfi_adjust_cfa_offset 16"); +#endif /* sp = get stack pointer from assembly code */ __asm__ ("mov %[result], sp" : [result] "=r" (sp)); /* store stack */ @@ -95,6 +104,9 @@ void *STACKMAN_SWITCH_INASM_NAME(stackman_cb_t callback, void *context) "msr fpcr, x1\n\t" "msr fpsr, x2" ::: "x1", "x2", "memory"); +#if !defined(__APPLE__) + __asm__ volatile (".cfi_adjust_cfa_offset -16"); +#endif return sp; } From 2b8ffea5aed7ed6c4c5a96e669274d7da003b597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Thu, 15 Oct 2026 12:34:07 +0000 Subject: [PATCH 09/11] Test that rounding mode is isolated across switches. test_09 sets FE_DOWNWARD, lets the callback switch to FE_TOWARDZERO, and checks that the caller sees FE_DOWNWARD again on ABIs that spill the floating-point control register. A second check saves a stack while that mode is current, switches away with FE_UPWARD live, and checks that the resumed copy still has FE_DOWNWARD. Soft-float arm32 does not spill FPSCR, and the test expects the callback's mode to remain. The test programs link libm for fesetround. --- AGENTS.md | 8 +++-- Makefile | 8 ++--- docs/platform-abi.md | 3 ++ tests/test.c | 73 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3b0616d..a59a404 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,10 +101,14 @@ make PLATFORM_PREFIX=arm-linux-gnueabihf EMULATOR=qemu-arm test Success ends with `*** All test suites passed ***`. Four programs: `bin/test`, `bin/test_cc`, `bin/test_static`, -`bin/test_asm`. `tests/test.c` is `test_01` through `test_08`. `test_04` +`bin/test_asm`. `tests/test.c` is `test_01` through `test_09`. `test_04` and `test_05` are the `stackman_call` contract. `test_07` and `test_08` pressure the compiler. They pass even when a callee-saved register is -dropped, if the compiler kept the values in memory. +dropped, if the compiler kept the values in memory. `test_09` checks +that `stackman_switch` restores the caller's rounding mode, including +across a stack switch. Soft-float `arm32` does not, and that test +expects the callback's mode to stick. The test programs link `-lm` +for `fesetround`. The linker warning `missing .note.GNU-stack section` on `tests/test_asm_s.o` is expected. diff --git a/Makefile b/Makefile index 65b8048..913436d 100644 --- a/Makefile +++ b/Makefile @@ -78,13 +78,13 @@ tests: bin/test_asm tests: LDLIBS := -lstackman bin/test: tests/test.o $(LIB)/libstackman.a - $(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $< ${DEBUG} $(LDLIBS) + $(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $< ${DEBUG} $(LDLIBS) -lm bin/test_cc: tests/test_cc.o $(LIB)/libstackman.a - $(CXX) $(LDFLAGS) $(STATIC_FLAG) -o $@ $< ${DEBUG} $(LDLIBS) + $(CXX) $(LDFLAGS) $(STATIC_FLAG) -o $@ $< ${DEBUG} $(LDLIBS) -lm bin/test_static: tests/test_static.o - $(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $^ ${DEBUG} + $(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $^ ${DEBUG} -lm bin/test_asm: tests/test_asm.o tests/test_asm_s.o - $(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $^ ${DEBUG} + $(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $^ ${DEBUG} -lm diff --git a/docs/platform-abi.md b/docs/platform-abi.md index 94cdae5..12fe996 100644 --- a/docs/platform-abi.md +++ b/docs/platform-abi.md @@ -26,6 +26,9 @@ stored, including caller-saved flag bits. The store is in the frame before that stack pointer is published to the callback. The reload is after `STACKMAN_OP_RESTORE` and before the epilogue pops callee-saved registers. `stackman_call` does not touch these registers. +`tests/test.c` `test_09` checks the caller's rounding mode. It comes +back on every ABI below except soft-float `arm32`, where the test +expects the callback's mode to remain. `STACKMAN_SP_ALIGN` follows `STACKMAN_STACK_ALIGN` below. diff --git a/tests/test.c b/tests/test.c index 6acd77e..b26aa13 100644 --- a/tests/test.c +++ b/tests/test.c @@ -1,11 +1,19 @@ #include "stackman.h" #include +#include #include #include #include #include +/* Soft-float arm32 does not spill FPSCR. Softfp selects that assembly. */ +#if defined(__arm__) && !defined(__aarch64__) && !defined(__ARM_PCS_VFP) +#define STACKMAN_ROUND_ISOLATED 0 +#else +#define STACKMAN_ROUND_ISOLATED 1 +#endif + #define assert_align(p) assert((intptr_t)(p) == STACKMAN_SP_ALIGN(p)) @@ -491,6 +499,69 @@ void test_08(void) assert(xor1 == xor2); } +/* The caller's rounding mode comes back from stackman_switch when the + * ABI spills the floating-point control register. The callback's change + * is still visible on STACKMAN_OP_RESTORE: the reload happens after + * that callback returns. stackman_call does not spill the register. + * Soft-float arm32 keeps the callback's mode. + */ +void *test_09_cb(void *context, int opcode, void *sp) +{ + int *saw = (int *)context; + + assert_align(sp); + if (opcode == (int)STACKMAN_OP_SAVE) { + assert(fegetround() == FE_DOWNWARD); + assert(fesetround(FE_TOWARDZERO) == 0); + *saw = 1; + return sp; + } + assert(opcode == (int)STACKMAN_OP_RESTORE); + assert(fegetround() == FE_TOWARDZERO); + return sp; +} + +void test_09(void *stack_marker) +{ + static jmp *saved; + int previous = fegetround(); + int saw = 0; + jmp *created; + + assert(fesetround(FE_DOWNWARD) == 0); + assert(fegetround() == FE_DOWNWARD); + stackman_switch(&test_09_cb, &saw); + assert(saw == 1); + if (STACKMAN_ROUND_ISOLATED) + assert(fegetround() == FE_DOWNWARD); + else + assert(fegetround() == FE_TOWARDZERO); + + /* The saved stack carries FE_DOWNWARD. This activation then + * switches away with FE_UPWARD live. Resuming the copy must + * see the mode that was saved with it. */ + assert(fesetround(FE_DOWNWARD) == 0); + created = jmp_save(stack_marker); + if (created) { + saved = created; + assert(fegetround() == FE_DOWNWARD); + assert(fesetround(FE_UPWARD) == 0); + jmp_jmp(saved); + assert(0); + } + + if (STACKMAN_ROUND_ISOLATED) + assert(fegetround() == FE_DOWNWARD); + else + assert(fegetround() == FE_UPWARD); + assert(fesetround(previous) == 0); + if (saved) { + free(saved->buf); + free(saved); + saved = 0; + } +} + int main(int argc, char*argv[]) { int stack_marker = 0; @@ -513,5 +584,7 @@ int main(int argc, char*argv[]) printf("test_07 ok\n"); test_08(); printf("test_08 ok\n"); + test_09((void*)&stack_marker); + printf("test_09 ok\n"); return 0; } From 8f89f8b45b0cdd0f3fae084a66d296d7cfaead4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Thu, 15 Oct 2026 15:28:18 +0000 Subject: [PATCH 10/11] Spill MXCSR in the Windows x86 stack switch. MSVC's fegetround reads MXCSR, so restoring only the x87 control word leaves the callback's rounding mode in place. test_09 hit that on Win32 and then sat in the CRT assert dialog. The switch now stores both registers in an 8-byte slot at the pointer passed to the callback. The workflow test steps now stop after one minute. A passing run finishes in a few seconds, and the dialog can no longer hold the job. --- .github/workflows/buildcommit.yml | 3 +++ AGENTS.md | 4 ++++ docs/platform-abi.md | 14 ++++++++------ stackman/platforms/switch_x86_msvc.asm | 15 ++++++++++----- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/.github/workflows/buildcommit.yml b/.github/workflows/buildcommit.yml index 68cc78c..0fe9f48 100644 --- a/.github/workflows/buildcommit.yml +++ b/.github/workflows/buildcommit.yml @@ -94,6 +94,7 @@ jobs: - name: make run: make all - name: test + timeout-minutes: 1 run: make test - name: set abi name run: echo abiname=$(make abiname) >> $GITHUB_ENV @@ -126,6 +127,7 @@ jobs: run: make all - name: Run tests + timeout-minutes: 1 run: make test - name: Upload artifacts @@ -160,6 +162,7 @@ jobs: run: msbuild.exe vs2022\stackman.sln /p:Platform=${{matrix.platform}} - name: test if: ${{ matrix.native == 'yes' }} + timeout-minutes: 1 run: vs2022\${{matrix.folder}}\Debug\test.exe - name: Upload build artifacts uses: actions/upload-artifact@v7.0.0 diff --git a/AGENTS.md b/AGENTS.md index a59a404..22f404e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -142,6 +142,10 @@ major version. `commit-artifacts` in `.github/workflows/buildbsd.yml` builds and tests FreeBSD, OpenBSD, and NetBSD with `gmake` on the `sysv_amd64` sources. +The test steps use `timeout-minutes: 1`. A passing run finishes in a few +seconds. On Windows an `assert` opens a CRT dialog and does not exit, so +that limit is what ends a failed `test.exe`. + A green native `make test` says nothing about another ABI. An assembly change is checked by the job that builds that file. diff --git a/docs/platform-abi.md b/docs/platform-abi.md index 12fe996..08cc358 100644 --- a/docs/platform-abi.md +++ b/docs/platform-abi.md @@ -146,12 +146,14 @@ Vector CSRs (`vcsr`, `vl`, `vtype`, `vxrm`, `vxsat`) are not spilled. ### `win_x86` -`stackman_switch_raw` spills `ebx`, `esi`, `edi`, `ebp`, and the x87 -control word (`fstcw` / `fldcw`). The C wrapper around it saves and -restores `fs:[0]`, the SEH chain. MXCSR is not spilled. - -`stackman_call` does not spill the x87 control word. The SEH save is -only on `stackman_switch`. +`stackman_switch_raw` spills `ebx`, `esi`, `edi`, `ebp`, the x87 +control word (`fstcw` / `fldcw`), and MXCSR (`stmxcsr` / `ldmxcsr`). +The published pointer is that 8-byte slot: the control word at `[esp]`, +MXCSR at `[esp+4]`. MSVC's `fegetround` reads MXCSR. The C wrapper +around the raw routine saves and restores `fs:[0]`, the SEH chain. + +`stackman_call` does not spill the x87 control word or MXCSR. The SEH +save is only on `stackman_switch`. ### `win_x64` diff --git a/stackman/platforms/switch_x86_msvc.asm b/stackman/platforms/switch_x86_msvc.asm index e24f4a1..2b7d975 100644 --- a/stackman/platforms/switch_x86_msvc.asm +++ b/stackman/platforms/switch_x86_msvc.asm @@ -1,6 +1,7 @@ .386 .model flat, c +.xmm .code @@ -11,11 +12,14 @@ stackman_switch_raw PROC callback:DWORD, context:DWORD push ebx push esi push edi - ; x87 control word sits at the pointer passed to the callback, - ; so the copied stack image carries it. Not in the C wrapper: - ; that frame does not travel with a real switch. - sub esp, 4 + ; x87 control word at [esp], MXCSR at [esp+4]. The published pointer + ; is this slot, so a copied stack image carries both. MSVC's + ; fegetround reads MXCSR; restoring the x87 word alone leaves that + ; mode behind. Not in the C wrapper: that frame does not travel + ; with a real switch. + sub esp, 8 fstcw word ptr [esp] + stmxcsr dword ptr [esp+4] mov esi, callback ; /* save 'callback' for later */ mov edi, context ; /* save 'context' for later */ @@ -41,7 +45,8 @@ stackman_switch_raw PROC callback:DWORD, context:DWORD add esp, 12 fldcw word ptr [esp] - add esp, 4 + ldmxcsr dword ptr [esp+4] + add esp, 8 pop edi pop esi From f2678a9beadb670db50f65aae05eb8706e7e0a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Thu, 15 Oct 2026 15:40:13 +0000 Subject: [PATCH 11/11] Say why the Windows x86 switch spills both FP controls. The x87 control word covers older compilers, which emit x87. MXCSR covers current MSVC, which emits SSE2. The slot layout is unchanged. --- docs/platform-abi.md | 6 ++++-- stackman/platforms/switch_x86_msvc.asm | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/platform-abi.md b/docs/platform-abi.md index 08cc358..3d3ddb3 100644 --- a/docs/platform-abi.md +++ b/docs/platform-abi.md @@ -149,8 +149,10 @@ Vector CSRs (`vcsr`, `vl`, `vtype`, `vxrm`, `vxsat`) are not spilled. `stackman_switch_raw` spills `ebx`, `esi`, `edi`, `ebp`, the x87 control word (`fstcw` / `fldcw`), and MXCSR (`stmxcsr` / `ldmxcsr`). The published pointer is that 8-byte slot: the control word at `[esp]`, -MXCSR at `[esp+4]`. MSVC's `fegetround` reads MXCSR. The C wrapper -around the raw routine saves and restores `fs:[0]`, the SEH chain. +MXCSR at `[esp+4]`. Both are spilled so older compilers, which emit +x87, and current MSVC, which emits SSE2, each keep their rounding mode. +The C wrapper around the raw routine saves and restores `fs:[0]`, the +SEH chain. `stackman_call` does not spill the x87 control word or MXCSR. The SEH save is only on `stackman_switch`. diff --git a/stackman/platforms/switch_x86_msvc.asm b/stackman/platforms/switch_x86_msvc.asm index 2b7d975..4af0d6c 100644 --- a/stackman/platforms/switch_x86_msvc.asm +++ b/stackman/platforms/switch_x86_msvc.asm @@ -13,10 +13,10 @@ stackman_switch_raw PROC callback:DWORD, context:DWORD push esi push edi ; x87 control word at [esp], MXCSR at [esp+4]. The published pointer - ; is this slot, so a copied stack image carries both. MSVC's - ; fegetround reads MXCSR; restoring the x87 word alone leaves that - ; mode behind. Not in the C wrapper: that frame does not travel - ; with a real switch. + ; is this slot, so a copied stack image carries both. Older 32-bit + ; compilers emit x87, so their rounding mode lives in the control + ; word. Current MSVC emits SSE2, so its mode lives in MXCSR. + ; Not in the C wrapper: that frame does not travel with a real switch. sub esp, 8 fstcw word ptr [esp] stmxcsr dword ptr [esp+4]