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 cd8cca1..22f404e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,15 +53,19 @@ 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`, `.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 - does not define `__ARM_PCS_VFP` and still builds as `arm32`. + `d8`–`d15` and the whole `FPSCR` in `stackman_switch`. Softfp + (`-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 @@ -78,8 +82,10 @@ 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`, 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 @@ -95,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. @@ -132,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/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/README.md b/README.md index 7ac261a..2d74611 100644 --- a/README.md +++ b/README.md @@ -106,9 +106,17 @@ 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`. +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 71ccd0e..c8381b2 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -1,5 +1,12 @@ # ABI register audit +This is the historical record of the register audit in +[pull request 21](https://github.com/stackless-dev/stackman/pull/21). +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. + 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. @@ -158,7 +165,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. | @@ -186,9 +193,6 @@ 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`. `FPSCR` is still not spilled. 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 @@ -359,8 +363,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`; `FPSCR` is still not spilled). +not added. A separate hard-float port is on the roadmap. ### Windows ARM32: delete it, or align `sp` before `vpush` @@ -397,20 +400,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. - -`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 -does not define `__ARM_PCS_VFP`, and the compiler can still use -`d8`–`d15` while building `arm32`. +`__ARM_PCS_VFP` is a compile error, so a hard-float build cannot use that +file by mistake. + +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 diff --git a/docs/platform-abi.md b/docs/platform-abi.md new file mode 100644 index 0000000..3d3ddb3 --- /dev/null +++ b/docs/platform-abi.md @@ -0,0 +1,166 @@ +# 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. +`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. + +## 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`. 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`. + +### `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. `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`. + +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`, 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]`. 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`. + +### `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`. diff --git a/stackman/platforms/switch_aarch64_gcc.S b/stackman/platforms/switch_aarch64_gcc.S index d2b9120..208610d 100644 --- a/stackman/platforms/switch_aarch64_gcc.S +++ b/stackman/platforms/switch_aarch64_gcc.S @@ -79,17 +79,28 @@ 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 +// 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 -// 67 "../platforms/switch_aarch64_gcc.h" 1 +// 93 "../platforms/switch_aarch64_gcc.h" 1 sub x1, sp, x0 mov sp, x0 sub x29, x29, x1 @@ -98,6 +109,14 @@ LABEL(stackman_switch) mov w1, 1 mov x0, x20 blr x19 +#APP +// 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 8e7c15c..9058c28 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 @@ -54,6 +59,27 @@ void *STACKMAN_SWITCH_INASM_NAME(stackman_cb_t callback, void *context) __asm__ volatile ("" : : : GP_PRESERVE); #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"); + /* 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)); @@ -70,6 +96,17 @@ 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"); +#if !defined(__APPLE__) + __asm__ volatile (".cfi_adjust_cfa_offset -16"); +#endif 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 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; } 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) diff --git a/stackman/platforms/switch_x86_msvc.asm b/stackman/platforms/switch_x86_msvc.asm index e24f4a1..4af0d6c 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. 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] 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 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; }