From 8870fbe5f47f8f6b9ed8db78377c0e22e7bbbdc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 09:39:49 +0000 Subject: [PATCH 01/15] Test stackman_call's return value and previous stack pointer. test_04 and test_05 now check that the callback result comes back unchanged, that a caller local survives writes on the alternate stack, and that the pointer argument is the previous stack. The audit note records the register findings these checks come from. qemu-riscv64 is expected to fail until switch_riscv64_gcc.S skips a null stack and passes the previous stack pointer. --- docs/abi-register-audit.md | 213 +++++++++++++++++++++++++++++++++++++ tests/test.c | 41 +++++-- 2 files changed, 244 insertions(+), 10 deletions(-) create mode 100644 docs/abi-register-audit.md diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md new file mode 100644 index 0000000..bdbd5e5 --- /dev/null +++ b/docs/abi-register-audit.md @@ -0,0 +1,213 @@ +# ABI register audit + +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. + +`stackman_switch` must spill the whole non-volatile set. The callback may +copy or replace the stack, and the epilogue may return to a different caller. +`stackman_call` returns to the same caller, so it only has to preserve the +non-volatile registers it modifies. + +Volatile (caller-saved) registers are free to change across either call. +A test that expects them to keep their values is checking the wrong contract. + +Sources: AAPCS64 (2025Q4 / 2026Q3), Microsoft ARM64 and ARM32 ABI notes, +Microsoft x64 calling convention, System V i386 and AMD64 ABIs, RISC-V psABI +LP64D. + +## AArch64 + +Base non-volatile set, shared by Linux, macOS, and Windows: + +- `x19`–`x28`, `x29` (`fp`), `x30` (`lr`) +- low 64 bits of `v8`–`v15` (`d8`–`d15`) + +The high 64 bits of `v8`–`v15` are volatile. `x18` is the platform register. +Portable assembly leaves it alone; spilling it and writing it back later does +not make a temporary use of it safe. On Windows user mode it is the TEB pointer +for the whole time user code runs. + +`FPCR` control bits are global thread state in AAPCS64. On Windows those same +bits (AHP, DN, FZ, RMode, and the trap-enable bits) are non-volatile. Full SVE +`z8`–`z23` and predicates `p4`–`p15` are callee-saved only for a function that +itself takes or returns scalable arguments. `stackman_switch` is an ordinary C +function, so the base rule applies. `FPMR`, `FFR`, and SME `ZT0` are +caller-saved. + +### `switch_aarch64_gcc.S` (Linux `aarch64`, macOS `darwin_arm64`) + +`stackman_switch` matches the base set. It allocates 160 bytes and spills +`x19`–`x28`, `x29`, `x30`, and `d8`–`d15`, then loads those slots back. It +never writes `x18`. `stackman_call` spills `x29`, `x30`, and `x19`, and holds +the old stack pointer in `x19`. + +Two gaps that are not missing base registers: + +- After `mov sp, x0` the live `x29` is left pointing at the old stack. The + Windows ARM64 and SysV x86-64 routines add the same delta to the frame + pointer. The epilogue still reloads the saved `x29`, so the caller receives + the stacked value. The frame chain is wrong only during + `STACKMAN_OP_RESTORE`. +- A stack switch is an asynchronous transfer of control. AAPCS64 says + `setjmp` / `longjmp` call `__arm_za_disable` so the SME lazy-save buffer + (`TPIDR2_EL0`) is not left aimed at the wrong stack. This file does not. + +### `switch_arm64_msvc.asm` (Windows `win_arm64`) + +The spill set matches the Windows document: `x19`–`x28`, `x29`, `x30`, +`d8`–`d15`, `FPCR`, and `x18`. The bug is the live value of `x18` during the +callbacks. + +`stackman_switch` stores the callback in `x18` and calls through it: + +```asm +mov x19, x0 +mov x18, x3 +blr x3 ; first callback, x18 is no longer the TEB +... +blr x18 ; second callback +``` + +`stackman_call` parks the old stack pointer in `x18` across `blr x3`. TLS, +the stack limit, `__chkstk`, and any Win32 call read the TEB through `x18`. +The epilogue writes the saved value back, so the original caller is repaired +only when the callback returns. + +Fix: keep `x18` unchanged. `stackman_switch` already spills `x19`–`x28`; use +one of those for the callback. `stackman_call` should spill some other +non-volatile (for example `x19`) and keep the old stack pointer there. + +The Windows file adjusts `fp` across the switch, which the SysV file does not. +It has no SEH unwind opcodes. The x64 MASM uses `NESTED_ENTRY` and +`.pushreg` / `save_xmm128`. That is an unwinding gap, separate from the +register values. + +## Other platforms + +| File | What it spills | Result | +| --- | --- | --- | +| `switch_x86_64_gcc.S` | `rbx`, `rbp`, `r12`–`r15`, x87 control word, MXCSR | Complete for SysV AMD64. XMM registers are volatile there. The frame pointer is adjusted with the stack delta. | +| `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. `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. | + +### RISC-V `stackman_call` control bugs + +The register spill in `stackman_call` (`ra` and `s0`) is enough for the +registers that routine writes. The control flow is not. + +```asm +mv t0, a0 +mv t1, a2 +mv sp, a2 ; runs even when a2 is 0 +... +mv a2, t1 ; third argument is the new stack, not the old one +jalr t0 +``` + +Every other port skips a null stack pointer and passes the previous stack +pointer. The function comment on `stackman_call` says the third argument is +the previous stack pointer (`stackman/stackman_switch.h`). The +`STACKMAN_OP_CALL` enum comment says the opposite: the pointer that was +provided to `stackman_call`. The implementations other than RISC-V follow the +function comment. + +A null stack on RISC-V sets `sp` to zero and then calls the callback. That +faults inside the callback prologue. The return value itself is left in `a0` +and would survive the epilogue on a non-null stack. + +## What the current tests cover + +`tests/test.c` already exercises a lot of the control path: + +- `test_01` checks that `stackman_switch` returns the `STACKMAN_OP_RESTORE` + result. The no-switch case returns `(void*)1`. +- `test_02` / `test_03` copy a stack out and jump back, which depends on the + non-volatile image being in the copied bytes. +- `test_04` calls `stackman_call` on a fresh block. The callback writes 256 + bytes on that stack and returns `(void *)(intptr_t)0x1234`. The test + checks the return value, that a caller local is unchanged, that a local in + the callback sits on the new block, and that the pointer argument is not + the stack passed to `stackman_call` and does not fall inside + `[block, stack)`. The old `STACKMAN_SP_LE(stack2, passed)` check was true + when `passed == stack2`, so the RISC-V bug satisfied it. +- `test_05` calls `stackman_call(..., 0)` and checks the same return value + and caller local. The pointer argument must be non-null and at or below a + caller local. On RISC-V this still faults: `sp` is set to zero before the + callback runs, so the failure is a crash rather than an assertion. +- `test_07` / `test_08` do floating-point and integer work around a no-op + `stackman_switch`. They hope the compiler keeps values in callee-saved + registers. If it keeps them in volatile registers or in memory, the test + passes even when the assembly drops a callee-saved register. + +Reading the epilogues, every port leaves the callback result in the ABI +return register (`x0`, `r0`, `a0`, `rax`, `eax`). `test_04` and `test_05` +now guard that. + +Linux CI (`.github/workflows/buildcommit.yml`, job `build-linux-gnu`) already +runs `make test` natively for x86-64, with `-m32` for i386, and under +`qemu-arm`, `qemu-aarch64`, and `qemu-riscv64`. macOS runs Intel and Apple +Silicon natively. Windows runs the test binary for x86 and x64 only. The +`arm64` Windows build is compiled and not executed (`matrix.native` is unset). + +## Tests + +The `stackman_call` contract checks are in `tests/test.c` (`test_04`, +`test_05`). They run on the existing Linux, qemu, and macOS jobs with no new +toolchains. `qemu-riscv64` is expected to fail until `switch_riscv64_gcc.S` +skips a null stack and passes the previous stack pointer: `test_04` asserts, +and `test_05` faults. Fix the `STACKMAN_OP_CALL` enum comment in the same +change, so both comments say the third argument is the previous stack pointer. + +Do not add a per-register assembly harness yet. + +### Assembly clobber harness + +A post-return sentinel check is the usual way to prove callee-saved +registers survive. It is a poor fit here. + +- It misses the Windows `x18` bug. That routine saves `x18` and writes it + back before `ret`. The damage is the live TEB during the callback. The + harness would see the restored value and pass. Sampling `x18` inside the + callback only means something on Windows, and the Windows ARM64 job does + not run tests. +- It needs a different `.S` or `.asm` for each ABI, kept in lockstep with + the spill list. The known mistakes above were found by reading those + lists against the ABI. A harness that encodes the same list will not + notice a register that both the implementation and the test omit. +- `test_07` and `test_08` already attempt the C version of this and can pass + for the wrong reason. Extending them will not make the signal much + stronger. +- SVE, SME, and shadow stacks need a specific qemu CPU model and a compiler + that emits that state. They are out of scope for a unit test of this + library's C ABI. + +If a harness is added later, limit it to the Linux ABIs the qemu jobs +already run (`sysv_amd64`, `sysv_i386`, `arm32`, `aarch64`, `riscv64`), and +only for the no-op switch: + +1. Assembly sets each callee-saved register to a sentinel, after saving the + real values for its own caller. +2. An assembly callback, not a C callback, overwrites those same registers. + A C callback is required to preserve them, so it cannot expose a missing + spill. +3. `stackman_switch` returns the same stack pointer. +4. The harness compares the sentinels, then restores the original registers. + +That catches "forgot to spill `d8`" on AArch64. It still does not catch +"`x18` was wrong for the duration of the call", and it does not belong in +the first patch. + +### Optional `fenv` check, still in C + +`fesetround` before `stackman_switch`, a callback that changes the rounding +mode and returns, then read the mode again. Expect the original mode where +the assembly saves the control word (SysV x86-64). Expect the callback's +mode to stick where the ABI treats that register as thread state and the +assembly does not save it (AAPCS64 `FPCR`, RISC-V `fcsr`). This is +ABI-specific and easy to get wrong; land the `stackman_call` contract tests +first. diff --git a/tests/test.c b/tests/test.c index f8a0057..6acd77e 100644 --- a/tests/test.c +++ b/tests/test.c @@ -192,21 +192,30 @@ void test_03(void *stack_marker) #define TEST_04 #endif #ifdef TEST_04 +/* Sentinel returned by stackman_call. The epilogue must leave it intact. */ +static void *const stackman_call_result = (void *)(intptr_t)0x1234; + /* test retrieval of stack pointer */ void *test_04_cb(void* context, int _opcode, void *old_sp) { + unsigned char scratch[256]; assert_align(old_sp); ctxt01 *c = (ctxt01*)context; stackman_op_t opcode = (stackman_op_t)_opcode; assert(opcode == STACKMAN_OP_CALL || opcode == 100); - /* one level of recursion here so that we actually use some of the new stack */ - if (opcode == STACKMAN_OP_CALL) + /* Write the stack the callback is running on. The caller's locals + * must still be intact when stackman_call returns. */ + if (opcode == STACKMAN_OP_CALL) { + memset(scratch, 0xa5, sizeof scratch); + assert(scratch[0] == 0xa5 && scratch[sizeof scratch - 1] == 0xa5); + /* one level of recursion here so that we actually use some of the new stack */ test_04_cb(context, 100, old_sp); + } c->sp[0] = &c; c->sp[1] = old_sp; - return 0; + return stackman_call_result; } /* test stackman_call() with a non-null stack pointer */ @@ -215,6 +224,8 @@ void test_04(void) char *block, *stack, *stack2; intptr_t stacksize=1024; int i, cnt; + int caller_local = 0x51a7; + void *result; ctxt01 ctxt; assert(STACKMAN_STACK_FULL_DESCENDING); @@ -232,14 +243,20 @@ void test_04(void) stack2 = stack-64; /* perform the call */ - stackman_call(test_04_cb, &ctxt, stack2); + result = stackman_call(test_04_cb, &ctxt, stack2); + assert(result == stackman_call_result); + assert(caller_local == 0x51a7); /* verify that the called function saw stack in the right range */ assert(STACKMAN_SP_LE(block, ctxt.sp[0])); assert(STACKMAN_SP_LS(ctxt.sp[0], stack2)); - /* verify that old stack was outside the stack we allocated */ - assert(STACKMAN_SP_LE(stack2, ctxt.sp[1]) || STACKMAN_SP_LS(ctxt.sp[1], block)); + /* The pointer passed to the callback is the previous stack pointer. + * It is not the stack given to stackman_call, and it does not fall + * inside the new block [block, stack). */ + assert(ctxt.sp[1] != (void *)stack2); + assert(STACKMAN_SP_LS(ctxt.sp[1], block) || + STACKMAN_SP_LE((void *)stack, ctxt.sp[1])); /* verify that the guard memory is correct */ cnt = 0; @@ -257,17 +274,21 @@ void test_04(void) void test_05(void) { ctxt01 ctxt; + int caller_local = 0x51a7; + void *result; assert(STACKMAN_STACK_FULL_DESCENDING); /* perform the call */ - stackman_call(test_04_cb, &ctxt, 0); + result = stackman_call(test_04_cb, &ctxt, 0); + assert(result == stackman_call_result); + assert(caller_local == 0x51a7); - /* verify that it was passed a stack */ + /* A null stack leaves the caller in place. The callback receives + * that live stack pointer, which is at or below the caller's locals. */ assert(ctxt.sp[1]); assert(STACKMAN_SP_LE(ctxt.sp[0], ctxt.sp[1])); - - /* and that it was passed valid lower stack pointer */ + assert(STACKMAN_SP_LE(ctxt.sp[1], (void *)&caller_local)); assert(STACKMAN_SP_LE(ctxt.sp[1], &ctxt)); } From e6c5e9249bb4fefd19648b737bc7531fb462a684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 10:21:57 +0000 Subject: [PATCH 02/15] update docs --- README.md | 6 +- docs/abi-register-audit.md | 227 ++++++++++++++++++++++++- stackman/platforms/switch_x86_64_gcc.h | 2 +- stackman/stackman_switch.h | 3 +- 4 files changed, 227 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1e770b8..240b420 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ platforms so that no assembly steps are required by users. straightforward implementation on most platforms. - Complex logic and branching is delegated to the C callback. - Custom platform assembly code must only do three things: - 1. Save and restore volatile registers and stack state on the stack + 1. Save and restore callee-saved (non-volatile) registers and stack state on the stack 2. Call the callback twice with the current stack pointer 3. Set the stack pointer to the value returned by the first callback. @@ -157,7 +157,9 @@ use. 1. It saves the current CPU stack pointer. If `stack` is non-zero, it will replace the stackpointer with that value. -2. It calls the callback function with the opcode `STACKMAN_OP_CALL`. +2. It calls the callback function with the opcode `STACKMAN_OP_CALL`. The + stack pointer argument is the previous stack pointer, the one in use before + this call. A null `stack` leaves that pointer unchanged. 3. It replaces the stack pointer with the previously saved value and returns the return value from the callback. This function is useful for at least three things: diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index bdbd5e5..e319554 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -16,6 +16,44 @@ Sources: AAPCS64 (2025Q4 / 2026Q3), Microsoft ARM64 and ARM32 ABI notes, Microsoft x64 calling convention, System V i386 and AMD64 ABIs, RISC-V psABI LP64D. +## Existing documentation + +There is no file that lists, for each ABI, which registers `stackman_switch` +spills. Those lists live in the assembly and in the clobber comments of the +inline headers. This note is the cross-check. + +The API is described in two places: + +- `README.md`, section "API", describes both functions. The `stackman_call` + steps now say that the callback receives the previous stack pointer. The + "Features" list now tells a new port to save callee-saved registers. It + previously said "volatile", which is the wrong set. +- `stackman/stackman_switch.h` is the implementation contract. Steps 1–6 for + `stackman_switch` match the assembly: spill callee-saved state, call with + `STACKMAN_OP_SAVE`, install the returned stack pointer, call with + `STACKMAN_OP_RESTORE`, pop the state, return the second result. The + `stackman_call` comment says the third argument is the previous stack + pointer. The `STACKMAN_OP_CALL` enumerator comment said the opposite (the + pointer passed into `stackman_call`). It now matches the function comment. + RISC-V still implements the old enumerator text. + +Per-platform comments, checked against the code: + +| Place | Claim | Matches the code | +| --- | --- | --- | +| `switch_x86_64_gcc.h` | `rbx`, `rbp`, `r12`–`r15`, x87 control word, MXCSR | Yes. The comment used to say "mmx"; the instruction is `stmxcsr`. | +| `switch_x86_gcc.h` | `ebx`, `esi`, `edi`, and `ebp` via the frame | Yes for what it saves. It does not mention the x87 control word. | +| `switch_aarch64_gcc.h` | `x19`–`x28`, `x29` via the frame, `d8`–`d15` | Yes for the base AAPCS64 set. Silent on `x18` and SME. | +| `switch_arm_gcc.h` | `r4`–`r11`, and `d8`–`d15` unless Thumb. `FPSCR` is not saved. | The integer list matches `switch_arm_gcc.S`. That `.S` is soft-float and does not save `d8`–`d15`, so `CP_REGISTERS` describes the inline path only. | +| `switch_riscv64_gcc.S` | `s0`–`s11`, `fs0`–`fs11`, `ra` for `stackman_switch` | Yes for that function. The `stackman_call` comments describe always switching and passing the new stack, which is the bug. | +| `switch_x64_msvc.asm` | "all registers that the x64 ABI specifies as non-volatile", plus xmm | The GPRs and `xmm6`–`xmm15` match. MXCSR[6:15] is non-volatile on Windows and is not saved, so the comment overclaims. | +| `switch_arm64_msvc.asm` | `x18`–`x28`, `d8`–`d15`, `FPCR` are pushed | The spill matches the comment. The routine then uses `x18` as a scratch register, which the Windows ABI does not allow. | + +`lib/README.md` is about prebuilt archives, not the API or the register sets. +Its platform list omits `riscv64` and the Darwin ABIs that the main README +lists. `CHANGELOG.md` records Windows ARM32 as dropped in 1.0.0. +`platform.h` still selects `switch_arm_msvc.h` when `_M_ARM` is set. + ## AArch64 Base non-volatile set, shared by Linux, macOS, and Windows: @@ -110,16 +148,191 @@ jalr t0 ``` Every other port skips a null stack pointer and passes the previous stack -pointer. The function comment on `stackman_call` says the third argument is -the previous stack pointer (`stackman/stackman_switch.h`). The -`STACKMAN_OP_CALL` enum comment says the opposite: the pointer that was -provided to `stackman_call`. The implementations other than RISC-V follow the -function comment. +pointer. The function comment on `stackman_call` in +`stackman/stackman_switch.h` says the third argument is the previous stack +pointer. The `STACKMAN_OP_CALL` enumerator comment used to say the pointer +that was provided to `stackman_call`; it now matches the function comment. +The implementations other than RISC-V follow that comment. A null stack on RISC-V sets `sp` to zero and then calls the callback. That faults inside the callback prologue. The return value itself is left in `a0` and would survive the epilogue on a non-null stack. +## Suggested fixes + +Each item is a local edit. The spill lists that already match the ABI stay +as they are. `stackman_call` only needs a new spill when the fix starts +using a callee-saved register it does not already save. + +### Windows ARM64: stop writing `x18` + +File: `stackman/platforms/switch_arm64_msvc.asm`. + +`stackman_switch` already spills `x19`–`x28` before it assigns them. Keep +the context in `x19`. Keep the callback in `x20` instead of `x18`, and make +the second call `blr x20`. The first call can stay `blr x3` only if `x20` +was loaded from `x3` first; the callback clobbers `x3`. Leave the `x18` +store and reload in place so a restored context gets the value it spilled, +and do not write `x18` between them. + +`stackman_call` should spill `x19` in the slot that now holds `x18` +(`str`/`ldr` at `[sp, #16]`), keep the old stack pointer in `x19`, and +delete every `x18` access. The 32-byte frame is already large enough. + +Unwind data is a separate change. Describe the prologue with the ARM64 +counterparts of the `NESTED_ENTRY` / `save_xmm128` macros in +`switch_x64_msvc.asm`, one unwind code per store, so `RtlUnwind` can find +`x19`–`x30`, `d8`–`d15`, and the `FPCR` slot. The Windows ARM64 test job +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. + +### AArch64 SysV and Apple: keep `x29` attached to the new stack + +File: `stackman/platforms/switch_aarch64_gcc.S`, in `stackman_switch`, +immediately after the first `blr` and before `mov w1, 1`. + +`x0` is the new stack pointer. `x19` is the callback and `x20` is the +context. `x1` is free until it becomes the opcode: + +```asm +sub x1, sp, x0 +mov sp, x0 +sub x29, x29, x1 +``` + +That is the same delta the Windows ARM64 file applies to `fp`. The epilogue +still reloads `x29` from the stack, so the caller receives the saved value, +not this adjusted one. The adjustment matters only while +`STACKMAN_OP_RESTORE` runs. A callback that relocates a stack still has to +rewrite saved frame-pointer slots in the copied bytes; this edit does not do +that. + +The checked-in `.S` is what default builds use (`STACKMAN_INLINE_ASM` is +unset). The inline sequence in `switch_aarch64_gcc.h` is the generator. Put +the same three instructions in that header if the `.S` is regenerated from +`gen_asm.c`, or the next regeneration will drop the delta. + +The same three-instruction pattern applies to `switch_arm_gcc.S` +(`sub r1, sp, r0` / `mov sp, r0` / `sub r11, r11, r1`) and to +`switch_riscv64_gcc.S` (`sub t0, sp, a0` before `mv sp, a0`, then add that +delta to `s0` only when `s0` is the live frame pointer). On RISC-V `s0` is +just a saved register in this function, not a frame chain, so the integer +spill is already complete and the delta is optional. Do not spend the +RISC-V edit on `s0` until the `stackman_call` control fix below is in. + +### AArch64 SME lazy save + +On entry to `stackman_switch`, before `sp` changes, call `__arm_za_disable` +through a weak reference when the symbol is present. That is what AAPCS64 +requires of `setjmp` / `longjmp`: commit the lazy ZA buffer and clear +`TPIDR2_EL0` before the stack that holds the buffer is abandoned. Do not +spill the ZA array in this file. `FPMR`, `FFR`, and `ZT0` stay +caller-saved. Skip the call on the Windows ARM64 file; this helper is the +AAPCS64 one. This can wait until a supported target actually enables SME. + +### RISC-V `stackman_call` + +File: `stackman/platforms/switch_riscv64_gcc.S`. `s0` already holds the +stack pointer after the 16-byte allocation, and it is spilled. Replace the +unconditional switch with: + +```asm +mv t0, a0 # callback +mv t1, a2 # requested stack, or zero +mv a0, a1 # context +li a1, 2 +mv a2, s0 # previous stack pointer +beqz t1, 1f +mv sp, t1 +1: +jalr t0 +mv sp, s0 +``` + +Set up `a0`–`a2` before `sp` changes so the arguments do not depend on the +new stack. `t0` is only needed for `jalr`. Update the comments that say +`a2` is the new stack. `test_04` should then assert a pass, and `test_05` +should return instead of faulting, under `qemu-riscv64`. + +### Windows x64: spill MXCSR with the xmm frame + +File: `stackman/platforms/switch_x64_msvc.asm`, `stackman_switch` only. + +`alloc_stack 168` covers `xmm6`–`xmm15` (160 bytes) plus an 8-byte pad at +`[rsp+160]`. After `alloc_stack` and before the `push_reg`s, `stmxcsr` +that dword. After the epilogue pops the GPRs and before `add rsp, 168`, +`ldmxcsr` the same slot. Restoring the whole register also restores the +volatile status flags, which is what `switch_x86_64_gcc.S` already does. +`stackman_call` does not need this: it returns to the same caller, and a +conforming callback restores MXCSR itself. + +Narrow the comment that says every non-volatile register is saved, once +MXCSR is included, or the comment stays ahead of the code. + +### i386: spill the x87 control word + +The integer spills are complete. The gap is `fnstcw` / `fldcw` around the +two callbacks in `stackman_switch`, stored inside the frame that the +callback is asked to copy. + +- `switch_x86_gcc.S` and the inline block in `switch_x86_gcc.h` must + change together. The `.S` is a copy of the header's asm string. The + `subl $0x1c, %esp` frame has unused bytes at `0x10(%esp)`, `0x14(%esp)`, + and `0x18(%esp)`, and `0x10(%esp)` is the pointer passed to the callback. + `fnstcw` a word at that pointer before the first `call`, and `fldcw` it + after the second `call`, before `addl $0x1c, %esp`. After + `leal -0x10(%eax), %esp` the same word is at `0x10(%esp)` on the new + stack. Regenerating with `gcc -S -m32 -fcf-protection=none` from + `gen_asm.c` is the project's usual way to refresh the `.S`. +- `switch_x86_msvc.asm`: `fnstcw` / `fldcw` in `stackman_switch_raw`, in a + slot allocated with `ebx` / `esi` / `edi`, so the word is on the stack + image rather than in the C wrapper's `seh_state` local. The wrapper + already restores `fs:[0]` and should keep doing that. + +XMM registers stay unsaved: they are volatile for SysV i386 and for +Windows cdecl. `__vectorcall` is not the convention this library uses. + +### ARM32 soft-float: refuse a hard-float build + +`switch_arm_gcc.S` is `.arch armv5t` and `.fpu softvfp`, which matches +`arm-linux-gnueabi`. Do not add `d8`–`d15` until there is an `armhf` ABI. +Do stop a hard-float compile from silently using this file. In +`switch_arm_gcc.h`, `#error` when `__ARM_PCS_VFP` is set, with a line that +says the checked-in assembly does not spill `d8`–`d15`. Leave +`CP_REGISTERS` as the inline-only clobber, or drop it, so it cannot be read +as a description of `switch_arm_gcc.S`. + +The live `r11` adjustment in the AArch64 section above is optional here. +The shipped integer set (`r4`–`r11`, `lr`) needs no new stores. + +### Windows ARM32: delete it, or align `sp` before `vpush` + +`CHANGELOG.md` says 1.0.0 dropped `win_arm`. The sources are still wired +up: `switch_arm_msvc.asm`, `switch_arm_msvc.h`, the `_M_ARM` branch in +`platform.h`, and the `win_arm` output directories in the VS projects. +Preferred fix: delete that wiring so the file cannot be built. + +If the file stays, the entry `sp` is 8-byte aligned and +`push {r4-r10, fp, lr}` subtracts 36 bytes, so `vpush {q4-q7}` runs at +4 mod 8. Push a pad so the count is even, then pad `FPSCR` the same way, +or the `blx` is 4 mod 8 again: + +```asm +push {r3, r4, r5, r6, r7, r8, r9, r10, fp, lr} ; 40 bytes +add fp, sp, #32 ; saved fp is the 9th word +vpush {q4-q7} +vmrs r2, fpscr +push {r2, r3} ; fpscr plus a pad +``` + +Pop in reverse, with `pc` in place of `lr`. `r3` is a pad; it does not +have to be preserved beyond the pop. + +`stackman_call` has the same call-site bug. +`push {r4, fp, lr}` is 12 bytes. Push one more register +(`push {r3, r4, fp, lr}`) and point `fp` at the saved frame pointer with +`add fp, sp, #8`. + ## What the current tests cover `tests/test.c` already exercises a lot of the control path: @@ -160,8 +373,8 @@ The `stackman_call` contract checks are in `tests/test.c` (`test_04`, `test_05`). They run on the existing Linux, qemu, and macOS jobs with no new toolchains. `qemu-riscv64` is expected to fail until `switch_riscv64_gcc.S` skips a null stack and passes the previous stack pointer: `test_04` asserts, -and `test_05` faults. Fix the `STACKMAN_OP_CALL` enum comment in the same -change, so both comments say the third argument is the previous stack pointer. +and `test_05` faults. The `STACKMAN_OP_CALL` enumerator comment already +matches that contract. Do not add a per-register assembly harness yet. diff --git a/stackman/platforms/switch_x86_64_gcc.h b/stackman/platforms/switch_x86_64_gcc.h index 6c89514..0357b79 100644 --- a/stackman/platforms/switch_x86_64_gcc.h +++ b/stackman/platforms/switch_x86_64_gcc.h @@ -51,7 +51,7 @@ void *STACKMAN_SWITCH_INASM_NAME(stackman_cb_t callback, void *context) { void *stack_pointer, *stack_pointer2; ptrdiff_t diff; - /* assembly to save non-volatile registers, including x87 and mmx */ + /* assembly to save non-volatile registers, including the x87 control word and MXCSR */ int mxcsr; short x87cw; __asm__ volatile ( "fstcw %[cw]\n\t" diff --git a/stackman/stackman_switch.h b/stackman/stackman_switch.h index cb83a3a..968f615 100644 --- a/stackman/stackman_switch.h +++ b/stackman/stackman_switch.h @@ -98,7 +98,8 @@ typedef enum stackman_op_t { STACKMAN_OP_RESTORE = 1, /* The callback is being invoked from a stackman_call() invocation. - * Stack_pointer is the pointer that was provided to stackman_call() + * stack_pointer is the stack pointer from before the call. A null + * stack argument to stackman_call() leaves that pointer unchanged. */ STACKMAN_OP_CALL = 2, From 38ec3a1c598141008680a90f65e4a116e36b4aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 10:46:36 +0000 Subject: [PATCH 03/15] fix riscv64 headers --- docs/abi-register-audit.md | 14 ++++++++++---- stackman/platforms/switch_riscv64_gcc.h | 15 +++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index e319554..184178a 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -371,10 +371,16 @@ Silicon natively. Windows runs the test binary for x86 and x64 only. The The `stackman_call` contract checks are in `tests/test.c` (`test_04`, `test_05`). They run on the existing Linux, qemu, and macOS jobs with no new -toolchains. `qemu-riscv64` is expected to fail until `switch_riscv64_gcc.S` -skips a null stack and passes the previous stack pointer: `test_04` asserts, -and `test_05` faults. The `STACKMAN_OP_CALL` enumerator comment already -matches that contract. +toolchains. On the first run of those tests, `qemu-riscv64` still printed +`test_03 ok` and then `test_06 ok`: `test_04` and `test_05` were not in the +binary. `STACKMAN_HAVE_CALL` lived inside the `STACKMAN_SWITCH_IMPL` branch +of `switch_riscv64_gcc.h`, so a normal `#include "stackman.h"` never defined +it and `tests/test.c` compiled the calls out. The macro is now set on that +include, with `STACKMAN_STACK_ALIGN` 16, matching the other platform headers. +After that, `qemu-riscv64` fails until `switch_riscv64_gcc.S` skips a null +stack and passes the previous stack pointer: `test_04` asserts, and +`test_05` faults. The `STACKMAN_OP_CALL` enumerator comment already matches +that contract. Do not add a per-register assembly harness yet. diff --git a/stackman/platforms/switch_riscv64_gcc.h b/stackman/platforms/switch_riscv64_gcc.h index 8e08597..a534f6c 100644 --- a/stackman/platforms/switch_riscv64_gcc.h +++ b/stackman/platforms/switch_riscv64_gcc.h @@ -10,17 +10,20 @@ #define STACKMAN_ASSEMBLY_SRC "platforms/switch_riscv64_gcc.S" #endif -#ifndef STACKMAN_SWITCH_IMPL -/* Just check if platform is supported */ -#define STACKMAN_SWITCH_INCLUDED -#else - +/* Visible to tests and other callers of stackman.h, not only to the + * translation unit that defines STACKMAN_SWITCH_IMPL. stackman.h + * otherwise leaves STACKMAN_STACK_ALIGN at 4 and tests/test.c compiles + * out test_04 and test_05. */ #ifndef STACKMAN_HAVE_CALL #define STACKMAN_HAVE_CALL 1 -#undef STACKMAN_STACK_ALIGN #define STACKMAN_STACK_ALIGN 16 #endif +#ifndef STACKMAN_SWITCH_IMPL +/* Just check if platform is supported */ +#define STACKMAN_SWITCH_INCLUDED +#else + #if __ASSEMBLER__ && defined(STACKMAN_ASSEMBLY_SRC) /* Include pre-generated assembly code */ #include STACKMAN_ASSEMBLY_SRC From 85d0bc31b9cc2f86b4bac2cd1ad8db02a08f4e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 10:51:26 +0000 Subject: [PATCH 04/15] Fix RISC-V stackman_call so the qemu tests can pass. A null stack was installed as sp, which faulted test_05, and the callback received that new pointer instead of the previous one, which failed test_04. Skip a zero stack and pass the saved stack pointer. --- docs/abi-register-audit.md | 78 +++++++------------------ stackman/platforms/switch_riscv64_gcc.S | 34 +++++++---- 2 files changed, 42 insertions(+), 70 deletions(-) diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 184178a..4b02e60 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -35,7 +35,8 @@ The API is described in two places: `stackman_call` comment says the third argument is the previous stack pointer. The `STACKMAN_OP_CALL` enumerator comment said the opposite (the pointer passed into `stackman_call`). It now matches the function comment. - RISC-V still implements the old enumerator text. + `switch_riscv64_gcc.S` now follows it: a null stack is left unchanged, + and the callback receives the previous stack pointer. Per-platform comments, checked against the code: @@ -45,7 +46,7 @@ Per-platform comments, checked against the code: | `switch_x86_gcc.h` | `ebx`, `esi`, `edi`, and `ebp` via the frame | Yes for what it saves. It does not mention the x87 control word. | | `switch_aarch64_gcc.h` | `x19`–`x28`, `x29` via the frame, `d8`–`d15` | Yes for the base AAPCS64 set. Silent on `x18` and SME. | | `switch_arm_gcc.h` | `r4`–`r11`, and `d8`–`d15` unless Thumb. `FPSCR` is not saved. | The integer list matches `switch_arm_gcc.S`. That `.S` is soft-float and does not save `d8`–`d15`, so `CP_REGISTERS` describes the inline path only. | -| `switch_riscv64_gcc.S` | `s0`–`s11`, `fs0`–`fs11`, `ra` for `stackman_switch` | Yes for that function. The `stackman_call` comments describe always switching and passing the new stack, which is the bug. | +| `switch_riscv64_gcc.S` | `s0`–`s11`, `fs0`–`fs11`, `ra` for `stackman_switch` | Yes for that function. `stackman_call` skips a null stack and passes the previous stack pointer. | | `switch_x64_msvc.asm` | "all registers that the x64 ABI specifies as non-volatile", plus xmm | The GPRs and `xmm6`–`xmm15` match. MXCSR[6:15] is non-volatile on Windows and is not saved, so the comment overclaims. | | `switch_arm64_msvc.asm` | `x18`–`x28`, `d8`–`d15`, `FPCR` are pushed | The spill matches the comment. The routine then uses `x18` as a scratch register, which the Windows ABI does not allow. | @@ -133,30 +134,16 @@ register values. | `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. | -### RISC-V `stackman_call` control bugs +### RISC-V `stackman_call` The register spill in `stackman_call` (`ra` and `s0`) is enough for the -registers that routine writes. The control flow is not. - -```asm -mv t0, a0 -mv t1, a2 -mv sp, a2 ; runs even when a2 is 0 -... -mv a2, t1 ; third argument is the new stack, not the old one -jalr t0 -``` - -Every other port skips a null stack pointer and passes the previous stack -pointer. The function comment on `stackman_call` in -`stackman/stackman_switch.h` says the third argument is the previous stack -pointer. The `STACKMAN_OP_CALL` enumerator comment used to say the pointer -that was provided to `stackman_call`; it now matches the function comment. -The implementations other than RISC-V follow that comment. - -A null stack on RISC-V sets `sp` to zero and then calls the callback. That -faults inside the callback prologue. The return value itself is left in `a0` -and would survive the epilogue on a non-null stack. +registers that routine writes. It used to switch `sp` even when the +requested stack was null, and it passed that new pointer as the third +argument. `test_04` then accepted `passed == stack2`, and `test_05` faulted +in the callback prologue. It now matches the other ports: a null stack is +left in place, and the callback receives the previous stack pointer (`s0`, +captured after the 16-byte frame allocation). The return value stays in +`a0`. ## Suggested fixes @@ -216,9 +203,10 @@ The same three-instruction pattern applies to `switch_arm_gcc.S` (`sub r1, sp, r0` / `mov sp, r0` / `sub r11, r11, r1`) and to `switch_riscv64_gcc.S` (`sub t0, sp, a0` before `mv sp, a0`, then add that delta to `s0` only when `s0` is the live frame pointer). On RISC-V `s0` is -just a saved register in this function, not a frame chain, so the integer -spill is already complete and the delta is optional. Do not spend the -RISC-V edit on `s0` until the `stackman_call` control fix below is in. +just a saved register in `stackman_switch`, not a frame chain, so the +integer spill is already complete and the delta is optional. `stackman_call` +uses `s0` to hold the previous stack pointer across the callback, so do not +adjust it there. ### AArch64 SME lazy save @@ -230,30 +218,6 @@ spill the ZA array in this file. `FPMR`, `FFR`, and `ZT0` stay caller-saved. Skip the call on the Windows ARM64 file; this helper is the AAPCS64 one. This can wait until a supported target actually enables SME. -### RISC-V `stackman_call` - -File: `stackman/platforms/switch_riscv64_gcc.S`. `s0` already holds the -stack pointer after the 16-byte allocation, and it is spilled. Replace the -unconditional switch with: - -```asm -mv t0, a0 # callback -mv t1, a2 # requested stack, or zero -mv a0, a1 # context -li a1, 2 -mv a2, s0 # previous stack pointer -beqz t1, 1f -mv sp, t1 -1: -jalr t0 -mv sp, s0 -``` - -Set up `a0`–`a2` before `sp` changes so the arguments do not depend on the -new stack. `t0` is only needed for `jalr`. Update the comments that say -`a2` is the new stack. `test_04` should then assert a pass, and `test_05` -should return instead of faulting, under `qemu-riscv64`. - ### Windows x64: spill MXCSR with the xmm frame File: `stackman/platforms/switch_x64_msvc.asm`, `stackman_switch` only. @@ -347,11 +311,10 @@ have to be preserved beyond the pop. the callback sits on the new block, and that the pointer argument is not the stack passed to `stackman_call` and does not fall inside `[block, stack)`. The old `STACKMAN_SP_LE(stack2, passed)` check was true - when `passed == stack2`, so the RISC-V bug satisfied it. + when `passed == stack2`, which is what the old RISC-V `stackman_call` did. - `test_05` calls `stackman_call(..., 0)` and checks the same return value and caller local. The pointer argument must be non-null and at or below a - caller local. On RISC-V this still faults: `sp` is set to zero before the - callback runs, so the failure is a crash rather than an assertion. + caller local. The old RISC-V code set `sp` to zero and faulted here. - `test_07` / `test_08` do floating-point and integer work around a no-op `stackman_switch`. They hope the compiler keeps values in callee-saved registers. If it keeps them in volatile registers or in memory, the test @@ -377,10 +340,9 @@ binary. `STACKMAN_HAVE_CALL` lived inside the `STACKMAN_SWITCH_IMPL` branch of `switch_riscv64_gcc.h`, so a normal `#include "stackman.h"` never defined it and `tests/test.c` compiled the calls out. The macro is now set on that include, with `STACKMAN_STACK_ALIGN` 16, matching the other platform headers. -After that, `qemu-riscv64` fails until `switch_riscv64_gcc.S` skips a null -stack and passes the previous stack pointer: `test_04` asserts, and -`test_05` faults. The `STACKMAN_OP_CALL` enumerator comment already matches -that contract. +The following `qemu-riscv64` run failed in `test_04` and `test_05`. +`switch_riscv64_gcc.S` now skips a null stack and passes the previous stack +pointer, which is the contract those tests check. Do not add a per-register assembly harness yet. diff --git a/stackman/platforms/switch_riscv64_gcc.S b/stackman/platforms/switch_riscv64_gcc.S index 54d97c1..3a504ec 100644 --- a/stackman/platforms/switch_riscv64_gcc.S +++ b/stackman/platforms/switch_riscv64_gcc.S @@ -166,15 +166,20 @@ LABEL(stackman_switch) .LFE0: SIZE_FUNCTION(stackman_switch) -/* +/* * void *stackman_call(stackman_cb_t callback, void *context, void *new_stack) - * - * Similar to stackman_switch but uses explicitly provided stack pointer. - * + * + * Calls callback(context, STACKMAN_OP_CALL, old_sp). A non-zero new_stack + * replaces sp for that call. Zero leaves the current stack in place. + * The third argument is the stack pointer from before the switch. + * * Register usage: - * - a0: callback function pointer - * - a1: context pointer - * - a2: new stack pointer + * - a0: callback function pointer, then context + * - a1: context pointer, then opcode + * - a2: requested stack pointer, then the previous stack pointer + * - s0: previous stack pointer (callee-saved) + * - t0: callback + * - t1: requested stack pointer */ .align 1 FUNCTION(stackman_call) @@ -195,15 +200,20 @@ LABEL(stackman_call) /* Save old stack pointer in s0 (callee-saved register) */ mv s0, sp - /* Setup args for callback and switch to new stack */ + /* Preserve the callback and the requested stack. Both are + * caller-saved, and both are consumed before jalr. */ mv t0, a0 /* t0 = callback */ - mv t1, a2 /* t1 = new_stack (for arg) */ - mv sp, a2 /* switch to new stack */ + mv t1, a2 /* t1 = requested stack, or zero */ - /* Call callback(context, opcode, sp) */ + /* callback(context, STACKMAN_OP_CALL, previous sp) */ mv a0, a1 /* a0 = context */ li a1, 2 /* a1 = STACKMAN_OP_CALL */ - mv a2, t1 /* a2 = new_stack */ + mv a2, s0 /* a2 = previous stack pointer */ + + /* A null stack means "keep the current one". */ + beqz t1, .Lcall_keep_sp + mv sp, t1 +.Lcall_keep_sp: jalr t0 /* call callback */ /* Restore old stack pointer from s0 */ From 2df8e0ab37c9709cc4eee1bd3e9f20fa7d6c79c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 11:07:59 +0000 Subject: [PATCH 05/15] Stop Windows ARM64 from writing the live TEB register. x18 is the TEB for the whole time user code runs. stackman_switch now keeps the callback in x20, and stackman_call keeps the old stack pointer in x19. Both routines still spill and restore x18 so a switched context gets the value it saved. SEH unwind codes are unchanged. --- docs/abi-register-audit.md | 68 +++++++----------------- stackman/platforms/switch_arm64_msvc.asm | 27 +++++----- 2 files changed, 32 insertions(+), 63 deletions(-) diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 4b02e60..41ea10e 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -48,7 +48,7 @@ Per-platform comments, checked against the code: | `switch_arm_gcc.h` | `r4`–`r11`, and `d8`–`d15` unless Thumb. `FPSCR` is not saved. | The integer list matches `switch_arm_gcc.S`. That `.S` is soft-float and does not save `d8`–`d15`, so `CP_REGISTERS` describes the inline path only. | | `switch_riscv64_gcc.S` | `s0`–`s11`, `fs0`–`fs11`, `ra` for `stackman_switch` | Yes for that function. `stackman_call` skips a null stack and passes the previous stack pointer. | | `switch_x64_msvc.asm` | "all registers that the x64 ABI specifies as non-volatile", plus xmm | The GPRs and `xmm6`–`xmm15` match. MXCSR[6:15] is non-volatile on Windows and is not saved, so the comment overclaims. | -| `switch_arm64_msvc.asm` | `x18`–`x28`, `d8`–`d15`, `FPCR` are pushed | The spill matches the comment. The routine then uses `x18` as a scratch register, which the Windows ABI does not allow. | +| `switch_arm64_msvc.asm` | `x18`–`x28`, `d8`–`d15`, `FPCR` are pushed | The spill matches the comment. `x18` is spilled and restored, and neither routine writes the live register. The callback is held in `x20`. `stackman_call` holds the old stack pointer in `x19`. | `lib/README.md` is about prebuilt archives, not the API or the register sets. Its platform list omits `riscv64` and the Darwin ABIs that the main README @@ -95,32 +95,22 @@ Two gaps that are not missing base registers: ### `switch_arm64_msvc.asm` (Windows `win_arm64`) The spill set matches the Windows document: `x19`–`x28`, `x29`, `x30`, -`d8`–`d15`, `FPCR`, and `x18`. The bug is the live value of `x18` during the -callbacks. +`d8`–`d15`, `FPCR`, and `x18`. `x18` is the TEB for the whole time user +code runs. Both routines spill it and leave the live register alone. -`stackman_switch` stores the callback in `x18` and calls through it: +`stackman_switch` keeps the context in `x19` and the callback in `x20`. +The first call is `blr x3` after `x20` has been loaded from `x3`, because +the callback clobbers `x3`. The second call is `blr x20`. The `x18` store +and reload stay, so a restored context gets the value it spilled. -```asm -mov x19, x0 -mov x18, x3 -blr x3 ; first callback, x18 is no longer the TEB -... -blr x18 ; second callback -``` - -`stackman_call` parks the old stack pointer in `x18` across `blr x3`. TLS, -the stack limit, `__chkstk`, and any Win32 call read the TEB through `x18`. -The epilogue writes the saved value back, so the original caller is repaired -only when the callback returns. +`stackman_call` spills `x19` at `[sp, #16]` and keeps the old stack pointer +there. It does not read or write `x18`. -Fix: keep `x18` unchanged. `stackman_switch` already spills `x19`–`x28`; use -one of those for the callback. `stackman_call` should spill some other -non-volatile (for example `x19`) and keep the old stack pointer there. - -The Windows file adjusts `fp` across the switch, which the SysV file does not. +The file adjusts `fp` across the switch, which the SysV file does not. It has no SEH unwind opcodes. The x64 MASM uses `NESTED_ENTRY` and `.pushreg` / `save_xmm128`. That is an unwinding gap, separate from the -register values. +register values. The Windows ARM64 test job compiles this file and does +not run it. ## Other platforms @@ -151,28 +141,6 @@ Each item is a local edit. The spill lists that already match the ABI stay as they are. `stackman_call` only needs a new spill when the fix starts using a callee-saved register it does not already save. -### Windows ARM64: stop writing `x18` - -File: `stackman/platforms/switch_arm64_msvc.asm`. - -`stackman_switch` already spills `x19`–`x28` before it assigns them. Keep -the context in `x19`. Keep the callback in `x20` instead of `x18`, and make -the second call `blr x20`. The first call can stay `blr x3` only if `x20` -was loaded from `x3` first; the callback clobbers `x3`. Leave the `x18` -store and reload in place so a restored context gets the value it spilled, -and do not write `x18` between them. - -`stackman_call` should spill `x19` in the slot that now holds `x18` -(`str`/`ldr` at `[sp, #16]`), keep the old stack pointer in `x19`, and -delete every `x18` access. The 32-byte frame is already large enough. - -Unwind data is a separate change. Describe the prologue with the ARM64 -counterparts of the `NESTED_ENTRY` / `save_xmm128` macros in -`switch_x64_msvc.asm`, one unwind code per store, so `RtlUnwind` can find -`x19`–`x30`, `d8`–`d15`, and the `FPCR` slot. The Windows ARM64 test job -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. - ### AArch64 SysV and Apple: keep `x29` attached to the new stack File: `stackman/platforms/switch_aarch64_gcc.S`, in `stackman_switch`, @@ -351,11 +319,11 @@ Do not add a per-register assembly harness yet. A post-return sentinel check is the usual way to prove callee-saved registers survive. It is a poor fit here. -- It misses the Windows `x18` bug. That routine saves `x18` and writes it - back before `ret`. The damage is the live TEB during the callback. The - harness would see the restored value and pass. Sampling `x18` inside the - callback only means something on Windows, and the Windows ARM64 job does - not run tests. +- A post-return check would have missed the old Windows `x18` bug. That + routine saved `x18` and wrote it back before `ret`. The damage was the + live TEB during the callback, so the harness would have seen the restored + value and passed. Sampling `x18` inside the callback only means something + on Windows, and the Windows ARM64 job does not run tests. - It needs a different `.S` or `.asm` for each ABI, kept in lockstep with the spill list. The known mistakes above were found by reading those lists against the ABI. A harness that encodes the same list will not @@ -379,7 +347,7 @@ only for the no-op switch: 3. `stackman_switch` returns the same stack pointer. 4. The harness compares the sentinels, then restores the original registers. -That catches "forgot to spill `d8`" on AArch64. It still does not catch +That catches "forgot to spill `d8`" on AArch64. It would not have caught "`x18` was wrong for the duration of the call", and it does not belong in the first patch. diff --git a/stackman/platforms/switch_arm64_msvc.asm b/stackman/platforms/switch_arm64_msvc.asm index b4b1f13..7608653 100644 --- a/stackman/platforms/switch_arm64_msvc.asm +++ b/stackman/platforms/switch_arm64_msvc.asm @@ -30,14 +30,17 @@ stp d14, d15, [sp, #160] ; args are x0=callback, x1=context - ; shuffle calling arguments into r0, r1 and r2 + ; shuffle calling arguments into x0, x1 and x2 ; for the call to the callback. + ; x18 is the TEB for the whole time user code runs. Spill it + ; above, and do not write the live register. x19 and x20 are + ; already spilled. mov x3, x0 mov x0, x1 ;context mov x2, sp ;stack pointer mov x1, #0 ;operation - mov x19, x0 ;store these a bit in nv registers - mov x18, x3 + mov x19, x0 ;context + mov x20, x3 ;callback (x3 is clobbered by the call) blr x3 ;first callback mov x2, x0 ;store new stackpointer in register, for next cb, ; switch stack, maintaining fp offset @@ -47,7 +50,7 @@ mov x1, #1 ;operation mov x0, x19 ;context - blr x18 ;second callback + blr x20 ;second callback ; restore registers from stack ldp x2, x18, [sp, #16] @@ -69,20 +72,18 @@ EXPORT |stackman_call| ALIGN |stackman_call| PROC -; 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. +; args "callback", "context", "stack" are in x0, x1, x2 + ; x29 is fp, x30 is lr. x18 is the TEB and is not touched. stp x29, x30, [sp, #-32]! ; store fp,lr pair, allocate stack space mov x29, sp ; and set fp pointing to old fp on stack - ; push non-volatile register 18 - str x18, [sp, #16] + str x19, [sp, #16] ; args are x0 = callback, x1 = context, x2=stack mov x3, x0 mov x0, x1 ;context mov x1, x2 ;stack - ; store current sp in nv register - mov x18, sp + ; store current sp in a spilled non-volatile + mov x19, sp mov x2, sp ; old stack @@ -94,10 +95,10 @@ nullptr blr x3 ; call callback, with context, opcode, old stack ; restore stack (could do: sub sp, fp #12) - mov sp, x18 + mov sp, x19 ; return - ldr x18, [sp, #16] + ldr x19, [sp, #16] ldp x29, x30, [sp], #32 ret ENDP From e8f34e77e6a12ece3066aece7d6d2210dabfed0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 11:10:09 +0000 Subject: [PATCH 06/15] Keep the AArch64 frame pointer attached across a stack switch. stackman_switch now applies the stack delta to x29 before the restore callback, in the checked-in assembly and in the inline sequence that generates it. The epilogue still reloads the saved x29. SME lazy save is left for a target that enables SME. --- docs/abi-register-audit.md | 66 ++++++++----------------- stackman/platforms/switch_aarch64_gcc.S | 4 +- stackman/platforms/switch_aarch64_gcc.h | 12 ++++- 3 files changed, 34 insertions(+), 48 deletions(-) diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 41ea10e..004b8b2 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -81,16 +81,17 @@ caller-saved. never writes `x18`. `stackman_call` spills `x29`, `x30`, and `x19`, and holds the old stack pointer in `x19`. -Two gaps that are not missing base registers: - -- After `mov sp, x0` the live `x29` is left pointing at the old stack. The - Windows ARM64 and SysV x86-64 routines add the same delta to the frame - pointer. The epilogue still reloads the saved `x29`, so the caller receives - the stacked value. The frame chain is wrong only during - `STACKMAN_OP_RESTORE`. -- A stack switch is an asynchronous transfer of control. AAPCS64 says - `setjmp` / `longjmp` call `__arm_za_disable` so the SME lazy-save buffer - (`TPIDR2_EL0`) is not left aimed at the wrong stack. This file does not. +`stackman_switch` applies the stack delta to the live `x29` before +`STACKMAN_OP_RESTORE` (`sub x1, sp, x0` / `mov sp, x0` / `sub x29, x29, x1`). +The same three instructions are in `switch_aarch64_gcc.h`, so regenerating +the `.S` from `gen_asm.c` keeps them. The epilogue still reloads the saved +`x29`, so the caller receives the stacked value. The adjustment matters only +during `STACKMAN_OP_RESTORE`. A callback that relocates a stack still has to +rewrite saved frame-pointer slots in the copied bytes. + +SME lazy save is not called. AAPCS64 says `setjmp` / `longjmp` call +`__arm_za_disable` so the ZA buffer (`TPIDR2_EL0`) is not left aimed at the +wrong stack. That can wait until a supported target enables SME. ### `switch_arm64_msvc.asm` (Windows `win_arm64`) @@ -106,7 +107,7 @@ and reload stay, so a restored context gets the value it spilled. `stackman_call` spills `x19` at `[sp, #16]` and keeps the old stack pointer there. It does not read or write `x18`. -The file adjusts `fp` across the switch, which the SysV file does not. +The file adjusts `fp` across the switch, as `switch_aarch64_gcc.S` does for `x29`. It has no SEH unwind opcodes. The x64 MASM uses `NESTED_ENTRY` and `.pushreg` / `save_xmm128`. That is an unwinding gap, separate from the register values. The Windows ARM64 test job compiles this file and does @@ -141,40 +142,15 @@ Each item is a local edit. The spill lists that already match the ABI stay as they are. `stackman_call` only needs a new spill when the fix starts using a callee-saved register it does not already save. -### AArch64 SysV and Apple: keep `x29` attached to the new stack - -File: `stackman/platforms/switch_aarch64_gcc.S`, in `stackman_switch`, -immediately after the first `blr` and before `mov w1, 1`. - -`x0` is the new stack pointer. `x19` is the callback and `x20` is the -context. `x1` is free until it becomes the opcode: - -```asm -sub x1, sp, x0 -mov sp, x0 -sub x29, x29, x1 -``` +### ARM32 and RISC-V live frame pointer -That is the same delta the Windows ARM64 file applies to `fp`. The epilogue -still reloads `x29` from the stack, so the caller receives the saved value, -not this adjusted one. The adjustment matters only while -`STACKMAN_OP_RESTORE` runs. A callback that relocates a stack still has to -rewrite saved frame-pointer slots in the copied bytes; this edit does not do -that. - -The checked-in `.S` is what default builds use (`STACKMAN_INLINE_ASM` is -unset). The inline sequence in `switch_aarch64_gcc.h` is the generator. Put -the same three instructions in that header if the `.S` is regenerated from -`gen_asm.c`, or the next regeneration will drop the delta. - -The same three-instruction pattern applies to `switch_arm_gcc.S` -(`sub r1, sp, r0` / `mov sp, r0` / `sub r11, r11, r1`) and to -`switch_riscv64_gcc.S` (`sub t0, sp, a0` before `mv sp, a0`, then add that -delta to `s0` only when `s0` is the live frame pointer). On RISC-V `s0` is -just a saved register in `stackman_switch`, not a frame chain, so the -integer spill is already complete and the delta is optional. `stackman_call` -uses `s0` to hold the previous stack pointer across the callback, so do not -adjust it there. +The AArch64 delta also applies to `switch_arm_gcc.S` +(`sub r1, sp, r0` / `mov sp, r0` / `sub r11, r11, r1`). That adjustment is +optional: the shipped integer set (`r4`–`r11`, `lr`) needs no new stores. +On RISC-V `s0` is just a saved register in `stackman_switch`, not a frame +chain, so the integer spill is already complete and the delta is skipped. +`stackman_call` uses `s0` to hold the previous stack pointer across the +callback, so do not adjust it there. ### AArch64 SME lazy save @@ -234,7 +210,7 @@ says the checked-in assembly does not spill `d8`–`d15`. Leave `CP_REGISTERS` as the inline-only clobber, or drop it, so it cannot be read as a description of `switch_arm_gcc.S`. -The live `r11` adjustment in the AArch64 section above is optional here. +The live `r11` adjustment in the section above is optional here. The shipped integer set (`r4`–`r11`, `lr`) needs no new stores. ### Windows ARM32: delete it, or align `sp` before `vpush` diff --git a/stackman/platforms/switch_aarch64_gcc.S b/stackman/platforms/switch_aarch64_gcc.S index 67fb984..d2b9120 100644 --- a/stackman/platforms/switch_aarch64_gcc.S +++ b/stackman/platforms/switch_aarch64_gcc.S @@ -89,8 +89,10 @@ LABEL(stackman_switch) blr x3 mov x2, x0 #APP -// 63 "../platforms/switch_aarch64_gcc.h" 1 +// 67 "../platforms/switch_aarch64_gcc.h" 1 + sub x1, sp, x0 mov sp, x0 + sub x29, x29, x1 // 0 "" 2 #NO_APP mov w1, 1 diff --git a/stackman/platforms/switch_aarch64_gcc.h b/stackman/platforms/switch_aarch64_gcc.h index 87d2a45..8e7c15c 100644 --- a/stackman/platforms/switch_aarch64_gcc.h +++ b/stackman/platforms/switch_aarch64_gcc.h @@ -59,8 +59,16 @@ void *STACKMAN_SWITCH_INASM_NAME(stackman_cb_t callback, void *context) __asm__ ("mov %[result], sp" : [result] "=r" (sp)); /* store stack */ sp = callback(context, STACKMAN_OP_SAVE, sp); - /* set stack pointer from sp using assembly */ - __asm__ ("mov sp, %[result]" : : [result] "r" (sp)); + /* Keep the live frame pointer attached to the new stack. + * x1 is free until the compiler assigns the RESTORE opcode. + * x29 is not an asm clobber: a clobber would make gcc save + * and restore it around this block and undo the delta. + */ + __asm__ ( + "sub x1, sp, %[result]\n\t" + "mov sp, %[result]\n\t" + "sub x29, x29, x1" + : : [result] "r" (sp) : "x1"); sp = callback(context, STACKMAN_OP_RESTORE, sp); return sp; } From a78e2880028309fe7ef4fe7b0b27ec9547762ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 11:10:59 +0000 Subject: [PATCH 07/15] Spill MXCSR in the Windows x64 stack switch. The control bits of MXCSR are non-volatile. stackman_switch stores the register in the unused 8-byte slot at the top of the xmm frame and loads it back before releasing that frame, including after the stack has moved. stackman_call returns to the same caller and does not spill it. --- docs/abi-register-audit.md | 19 ++----------------- stackman/platforms/switch_x64_msvc.asm | 10 ++++++---- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 004b8b2..505805d 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -47,7 +47,7 @@ Per-platform comments, checked against the code: | `switch_aarch64_gcc.h` | `x19`–`x28`, `x29` via the frame, `d8`–`d15` | Yes for the base AAPCS64 set. Silent on `x18` and SME. | | `switch_arm_gcc.h` | `r4`–`r11`, and `d8`–`d15` unless Thumb. `FPSCR` is not saved. | The integer list matches `switch_arm_gcc.S`. That `.S` is soft-float and does not save `d8`–`d15`, so `CP_REGISTERS` describes the inline path only. | | `switch_riscv64_gcc.S` | `s0`–`s11`, `fs0`–`fs11`, `ra` for `stackman_switch` | Yes for that function. `stackman_call` skips a null stack and passes the previous stack pointer. | -| `switch_x64_msvc.asm` | "all registers that the x64 ABI specifies as non-volatile", plus xmm | The GPRs and `xmm6`–`xmm15` match. MXCSR[6:15] is non-volatile on Windows and is not saved, so the comment overclaims. | +| `switch_x64_msvc.asm` | non-volatile GPRs, `xmm6`–`xmm15`, and MXCSR | Yes. The control bits of MXCSR are saved with the xmm frame. Upper ymm/zmm bits stay volatile. | | `switch_arm64_msvc.asm` | `x18`–`x28`, `d8`–`d15`, `FPCR` are pushed | The spill matches the comment. `x18` is spilled and restored, and neither routine writes the live register. The callback is held in `x20`. `stackman_call` holds the old stack pointer in `x19`. | `lib/README.md` is about prebuilt archives, not the API or the register sets. @@ -118,7 +118,7 @@ not run it. | File | What it spills | Result | | --- | --- | --- | | `switch_x86_64_gcc.S` | `rbx`, `rbp`, `r12`–`r15`, x87 control word, MXCSR | Complete for SysV AMD64. XMM registers are volatile there. The frame pointer is adjusted with the stack delta. | -| `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_x64_msvc.asm` | `rbx`, `rbp`, `rdi`, `rsi`, `r12`–`r15`, `xmm6`–`xmm15`, MXCSR | Those registers match the Windows x64 ABI. MXCSR is stored at `[rsp+160]`, the pad in the 168-byte xmm frame, and loaded back before that frame is released. `stackman_call` does not spill it. | | `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. `armhf` is not a configured ABI. The live frame pointer is not adjusted. | @@ -162,21 +162,6 @@ spill the ZA array in this file. `FPMR`, `FFR`, and `ZT0` stay caller-saved. Skip the call on the Windows ARM64 file; this helper is the AAPCS64 one. This can wait until a supported target actually enables SME. -### Windows x64: spill MXCSR with the xmm frame - -File: `stackman/platforms/switch_x64_msvc.asm`, `stackman_switch` only. - -`alloc_stack 168` covers `xmm6`–`xmm15` (160 bytes) plus an 8-byte pad at -`[rsp+160]`. After `alloc_stack` and before the `push_reg`s, `stmxcsr` -that dword. After the epilogue pops the GPRs and before `add rsp, 168`, -`ldmxcsr` the same slot. Restoring the whole register also restores the -volatile status flags, which is what `switch_x86_64_gcc.S` already does. -`stackman_call` does not need this: it returns to the same caller, and a -conforming callback restores MXCSR itself. - -Narrow the comment that says every non-volatile register is saved, once -MXCSR is included, or the comment stays ahead of the code. - ### i386: spill the x87 control word The integer spills are complete. The gap is `fnstcw` / `fldcw` around the diff --git a/stackman/platforms/switch_x64_msvc.asm b/stackman/platforms/switch_x64_msvc.asm index 1ff34dc..be0e757 100644 --- a/stackman/platforms/switch_x64_msvc.asm +++ b/stackman/platforms/switch_x64_msvc.asm @@ -20,10 +20,11 @@ endm ;arguments callback, context, are passed in rcx, rdx, respectively ;stackman_switch PROC FRAME NESTED_ENTRY stackman_switch, _TEXT$00 - ; save all registers that the x64 ABI specifies as non-volatile. - ; This includes some mmx registers. May not always be necessary, - ; unless our application is doing 3D, but better safe than sorry. - alloc_stack 168; 10 * 16 bytes, plus 8 bytes to make stack 16 byte aligned + ; Save the non-volatile GPRs, xmm6-xmm15, and MXCSR. + ; Upper ymm/zmm bits are volatile. MXCSR status flags are volatile + ; too; saving the whole register matches the SysV file. + alloc_stack 168; xmm6-xmm15 (160 bytes) plus an 8-byte MXCSR slot + stmxcsr dword ptr [rsp+160] save_xmm128 xmm15, 144 save_xmm128 xmm14, 128 save_xmm128 xmm13, 112 @@ -94,6 +95,7 @@ NESTED_ENTRY stackman_switch, _TEXT$00 load_xmm128 xmm8, 32 load_xmm128 xmm7, 16 load_xmm128 xmm6, 0 + ldmxcsr dword ptr [rsp+160] add rsp, 168 ret From b73a5822786510c64d620578b4ad4ad798f0d13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 11:13:51 +0000 Subject: [PATCH 08/15] Spill the i386 x87 control word with the stack image. SysV stores it with fstcw at the pointer passed to the callback, in both the checked-in assembly and the inline block it is copied from. Windows does the same inside stackman_switch_raw, next to the callee-saved pushes, and the C wrapper still restores the SEH chain. XMM registers stay unsaved. --- docs/abi-register-audit.md | 40 ++++++-------------------- stackman/platforms/switch_x86_gcc.S | 4 ++- stackman/platforms/switch_x86_gcc.h | 7 ++++- stackman/platforms/switch_x86_msvc.asm | 8 ++++++ 4 files changed, 26 insertions(+), 33 deletions(-) diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 505805d..06ea1b0 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -43,7 +43,7 @@ Per-platform comments, checked against the code: | Place | Claim | Matches the code | | --- | --- | --- | | `switch_x86_64_gcc.h` | `rbx`, `rbp`, `r12`–`r15`, x87 control word, MXCSR | Yes. The comment used to say "mmx"; the instruction is `stmxcsr`. | -| `switch_x86_gcc.h` | `ebx`, `esi`, `edi`, and `ebp` via the frame | Yes for what it saves. It does not mention the x87 control word. | +| `switch_x86_gcc.h` | `ebx`, `esi`, `edi`, `ebp` via the frame, x87 control word | Yes. The control word is stored with `fstcw` at the pointer passed to the callback. | | `switch_aarch64_gcc.h` | `x19`–`x28`, `x29` via the frame, `d8`–`d15` | Yes for the base AAPCS64 set. Silent on `x18` and SME. | | `switch_arm_gcc.h` | `r4`–`r11`, and `d8`–`d15` unless Thumb. `FPSCR` is not saved. | The integer list matches `switch_arm_gcc.S`. That `.S` is soft-float and does not save `d8`–`d15`, so `CP_REGISTERS` describes the inline path only. | | `switch_riscv64_gcc.S` | `s0`–`s11`, `fs0`–`fs11`, `ra` for `stackman_switch` | Yes for that function. `stackman_call` skips a null stack and passes the previous stack pointer. | @@ -119,8 +119,8 @@ not run it. | --- | --- | --- | | `switch_x86_64_gcc.S` | `rbx`, `rbp`, `r12`–`r15`, x87 control word, MXCSR | Complete for SysV AMD64. XMM registers are volatile there. The frame pointer is adjusted with the stack delta. | | `switch_x64_msvc.asm` | `rbx`, `rbp`, `rdi`, `rsi`, `r12`–`r15`, `xmm6`–`xmm15`, MXCSR | Those registers match the Windows x64 ABI. MXCSR is stored at `[rsp+160]`, the pad in the 168-byte xmm frame, and loaded back before that frame is released. `stackman_call` does not spill it. | -| `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_x86_gcc.S` | `ebx`, `esi`, `edi`, `ebp`, x87 control word | Integer set is complete. There is no callee-saved XMM on i386 SysV. `fstcw` stores the control word at `0x10(%esp)`, the pointer passed to the callback, and `fldcw` loads it after the restore callback. | +| `switch_x86_msvc.asm` | `ebx`, `esi`, `edi`, `ebp`, x87 control word, plus the C wrapper saves `fs:[0]` | Integer set is complete for cdecl. `stackman_switch_raw` stores the control word in a slot below the callee-saved pushes, at the pointer passed to the callback. The C wrapper still restores `fs:[0]`. | | `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. | @@ -162,29 +162,6 @@ spill the ZA array in this file. `FPMR`, `FFR`, and `ZT0` stay caller-saved. Skip the call on the Windows ARM64 file; this helper is the AAPCS64 one. This can wait until a supported target actually enables SME. -### i386: spill the x87 control word - -The integer spills are complete. The gap is `fnstcw` / `fldcw` around the -two callbacks in `stackman_switch`, stored inside the frame that the -callback is asked to copy. - -- `switch_x86_gcc.S` and the inline block in `switch_x86_gcc.h` must - change together. The `.S` is a copy of the header's asm string. The - `subl $0x1c, %esp` frame has unused bytes at `0x10(%esp)`, `0x14(%esp)`, - and `0x18(%esp)`, and `0x10(%esp)` is the pointer passed to the callback. - `fnstcw` a word at that pointer before the first `call`, and `fldcw` it - after the second `call`, before `addl $0x1c, %esp`. After - `leal -0x10(%eax), %esp` the same word is at `0x10(%esp)` on the new - stack. Regenerating with `gcc -S -m32 -fcf-protection=none` from - `gen_asm.c` is the project's usual way to refresh the `.S`. -- `switch_x86_msvc.asm`: `fnstcw` / `fldcw` in `stackman_switch_raw`, in a - slot allocated with `ebx` / `esi` / `edi`, so the word is on the stack - image rather than in the C wrapper's `seh_state` local. The wrapper - already restores `fs:[0]` and should keep doing that. - -XMM registers stay unsaved: they are volatile for SysV i386 and for -Windows cdecl. `__vectorcall` is not the convention this library uses. - ### ARM32 soft-float: refuse a hard-float build `switch_arm_gcc.S` is `.arch armv5t` and `.fpu softvfp`, which matches @@ -316,8 +293,9 @@ the first patch. `fesetround` before `stackman_switch`, a callback that changes the rounding mode and returns, then read the mode again. Expect the original mode where -the assembly saves the control word (SysV x86-64). Expect the callback's -mode to stick where the ABI treats that register as thread state and the -assembly does not save it (AAPCS64 `FPCR`, RISC-V `fcsr`). This is -ABI-specific and easy to get wrong; land the `stackman_call` contract tests -first. +the assembly saves the control word (SysV x86-64 MXCSR and x87 control +word, SysV i386 x87 control word, Windows x64 MXCSR, Windows x86 x87 +control word). Expect the callback's mode to stick where the ABI treats +that register as thread state and the assembly does not save it (AAPCS64 +`FPCR`, RISC-V `fcsr`). This is ABI-specific and easy to get wrong; land +the `stackman_call` contract tests first. diff --git a/stackman/platforms/switch_x86_gcc.S b/stackman/platforms/switch_x86_gcc.S index 671a6b5..558b7b9 100644 --- a/stackman/platforms/switch_x86_gcc.S +++ b/stackman/platforms/switch_x86_gcc.S @@ -30,11 +30,12 @@ stackman_switch: movl 8(%ebp), %eax movl 12(%ebp), %edx #APP -# 50 "../platforms/switch_x86_gcc.h" 1 +# 53 "../platforms/switch_x86_gcc.h" 1 subl $0x1c, %esp movl %eax, %esi movl %edx, %edi leal 0x10(%esp), %ebx +fstcw 0x10(%esp) movl %ebx, 8(%esp) movl $0, 4(%esp) movl %edi, 0(%esp) @@ -48,6 +49,7 @@ movl $1, 4(%esp) movl %edi, 0(%esp) call *%esi movl %eax, %eax +fldcw 0x10(%esp) addl $0x1c, %esp # 0 "" 2 diff --git a/stackman/platforms/switch_x86_gcc.h b/stackman/platforms/switch_x86_gcc.h index 14a3858..dc45133 100644 --- a/stackman/platforms/switch_x86_gcc.h +++ b/stackman/platforms/switch_x86_gcc.h @@ -21,7 +21,10 @@ /* Follow System V i386 abi, including 16 byte stack alignment * https://wiki.osdev.org/System_V_ABI#i386 * eax, ecx, edx are scratch regs, - * ebp, ebx, esi, edi are callee-preserved + * ebp, ebx, esi, edi and the x87 control word are callee-preserved. + * The control word is stored at the stack pointer passed to the callback + * so a copied stack image carries it. fstcw does not wait on pending + * exceptions, matching switch_x86_64_gcc.S. * We have compiler construct a frame and push ebp * an then we fix an aligned stack pointer and just store * the arguments in proper places. @@ -65,6 +68,7 @@ void *result; /* first call */ "leal 0x10(%%esp), %%ebx\n" /* arg 2 adjusted sp (also in nv. reg) */ + "fstcw 0x10(%%esp)\n" /* x87 control word, on the copied image */ "movl %%ebx, 8(%%esp)\n" "movl $0, 4(%%esp)\n" /* arg 1 opcode STACKMAN_OP_SAVE */ "movl %%edi, 0(%%esp)\n" /* arg 0 context */ @@ -86,6 +90,7 @@ void *result; "movl %%eax, %[result]\n" + "fldcw 0x10(%%esp)\n" "addl $0x1c, %%esp\n" : [result] "=r" (result) /* output variables */ : [cb] "r" (callback), /* input variables */ diff --git a/stackman/platforms/switch_x86_msvc.asm b/stackman/platforms/switch_x86_msvc.asm index 2f28cf9..e24f4a1 100644 --- a/stackman/platforms/switch_x86_msvc.asm +++ b/stackman/platforms/switch_x86_msvc.asm @@ -11,6 +11,11 @@ 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 + fstcw word ptr [esp] mov esi, callback ; /* save 'callback' for later */ mov edi, context ; /* save 'context' for later */ @@ -35,6 +40,9 @@ stackman_switch_raw PROC callback:DWORD, context:DWORD call esi ; /* call callback() */ add esp, 12 + fldcw word ptr [esp] + add esp, 4 + pop edi pop esi pop ebx From 3afd5c0f5ff3bd3352b0f5d8037de88fa44c9eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 11:15:51 +0000 Subject: [PATCH 09/15] Reject ARM32 hard-float and keep r11 attached across a switch. The checked-in assembly is soft-float and does not spill d8-d15, so a __ARM_PCS_VFP include is now a compile error. stackman_switch applies the stack delta to r11 before the restore callback. The RISC-V s0 delta stays omitted: s0 is not a frame chain there, and stackman_call uses it for the previous stack pointer. --- docs/abi-register-audit.md | 29 +++-------------------------- stackman/platforms/switch_arm_gcc.S | 4 +++- stackman/platforms/switch_arm_gcc.h | 29 +++++++++++++++++++++-------- 3 files changed, 27 insertions(+), 35 deletions(-) diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 06ea1b0..7e072e0 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -45,7 +45,7 @@ Per-platform comments, checked against the code: | `switch_x86_64_gcc.h` | `rbx`, `rbp`, `r12`–`r15`, x87 control word, MXCSR | Yes. The comment used to say "mmx"; the instruction is `stmxcsr`. | | `switch_x86_gcc.h` | `ebx`, `esi`, `edi`, `ebp` via the frame, x87 control word | Yes. The control word is stored with `fstcw` at the pointer passed to the callback. | | `switch_aarch64_gcc.h` | `x19`–`x28`, `x29` via the frame, `d8`–`d15` | Yes for the base AAPCS64 set. Silent on `x18` and SME. | -| `switch_arm_gcc.h` | `r4`–`r11`, and `d8`–`d15` unless Thumb. `FPSCR` is not saved. | The integer list matches `switch_arm_gcc.S`. That `.S` is soft-float and does not save `d8`–`d15`, so `CP_REGISTERS` describes the inline path only. | +| `switch_arm_gcc.h` | `r4`–`r11`, and `d8`–`d15` unless Thumb. `FPSCR` is not saved. | The integer list matches `switch_arm_gcc.S`. `CP_REGISTERS` is the inline clobber only; that `.S` is soft-float and does not save `d8`–`d15`. `__ARM_PCS_VFP` is a compile error. | | `switch_riscv64_gcc.S` | `s0`–`s11`, `fs0`–`fs11`, `ra` for `stackman_switch` | Yes for that function. `stackman_call` skips a null stack and passes the previous stack pointer. | | `switch_x64_msvc.asm` | non-volatile GPRs, `xmm6`–`xmm15`, and MXCSR | Yes. The control bits of MXCSR are saved with the xmm frame. Upper ymm/zmm bits stay volatile. | | `switch_arm64_msvc.asm` | `x18`–`x28`, `d8`–`d15`, `FPCR` are pushed | The spill matches the comment. `x18` is spilled and restored, and neither routine writes the live register. The callback is held in `x20`. `stackman_call` holds the old stack pointer in `x19`. | @@ -121,9 +121,9 @@ not run it. | `switch_x64_msvc.asm` | `rbx`, `rbp`, `rdi`, `rsi`, `r12`–`r15`, `xmm6`–`xmm15`, MXCSR | Those registers match the Windows x64 ABI. MXCSR is stored at `[rsp+160]`, the pad in the 168-byte xmm frame, and loaded back before that frame is released. `stackman_call` does not spill it. | | `switch_x86_gcc.S` | `ebx`, `esi`, `edi`, `ebp`, x87 control word | Integer set is complete. There is no callee-saved XMM on i386 SysV. `fstcw` stores the control word at `0x10(%esp)`, the pointer passed to the callback, and `fldcw` loads it after the restore callback. | | `switch_x86_msvc.asm` | `ebx`, `esi`, `edi`, `ebp`, x87 control word, plus the C wrapper saves `fs:[0]` | Integer set is complete for cdecl. `stackman_switch_raw` stores the control word in a slot below the callee-saved pushes, at the pointer passed to the callback. The C wrapper still restores `fs:[0]`. | -| `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_gcc.S` | `r4`–`r11`, `lr` | Complete for the soft-float `arm-linux-gnueabi` ABI this tree ships (`.arch armv5t`, `.fpu softvfp`). `d8`–`d15` are not spilled, and `__ARM_PCS_VFP` is a compile error. The live `r11` is adjusted with the stack delta before `STACKMAN_OP_RESTORE`; the epilogue still pops the saved value. | | `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. | +| `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. The optional `s0` frame delta is not applied: `s0` is not a frame chain there, and `stackman_call` uses it to hold the previous stack pointer. | ### RISC-V `stackman_call` @@ -142,16 +142,6 @@ Each item is a local edit. The spill lists that already match the ABI stay as they are. `stackman_call` only needs a new spill when the fix starts using a callee-saved register it does not already save. -### ARM32 and RISC-V live frame pointer - -The AArch64 delta also applies to `switch_arm_gcc.S` -(`sub r1, sp, r0` / `mov sp, r0` / `sub r11, r11, r1`). That adjustment is -optional: the shipped integer set (`r4`–`r11`, `lr`) needs no new stores. -On RISC-V `s0` is just a saved register in `stackman_switch`, not a frame -chain, so the integer spill is already complete and the delta is skipped. -`stackman_call` uses `s0` to hold the previous stack pointer across the -callback, so do not adjust it there. - ### AArch64 SME lazy save On entry to `stackman_switch`, before `sp` changes, call `__arm_za_disable` @@ -162,19 +152,6 @@ spill the ZA array in this file. `FPMR`, `FFR`, and `ZT0` stay caller-saved. Skip the call on the Windows ARM64 file; this helper is the AAPCS64 one. This can wait until a supported target actually enables SME. -### ARM32 soft-float: refuse a hard-float build - -`switch_arm_gcc.S` is `.arch armv5t` and `.fpu softvfp`, which matches -`arm-linux-gnueabi`. Do not add `d8`–`d15` until there is an `armhf` ABI. -Do stop a hard-float compile from silently using this file. In -`switch_arm_gcc.h`, `#error` when `__ARM_PCS_VFP` is set, with a line that -says the checked-in assembly does not spill `d8`–`d15`. Leave -`CP_REGISTERS` as the inline-only clobber, or drop it, so it cannot be read -as a description of `switch_arm_gcc.S`. - -The live `r11` adjustment in the section above is optional here. -The shipped integer set (`r4`–`r11`, `lr`) needs no new stores. - ### Windows ARM32: delete it, or align `sp` before `vpush` `CHANGELOG.md` says 1.0.0 dropped `win_arm`. The sources are still wired diff --git a/stackman/platforms/switch_arm_gcc.S b/stackman/platforms/switch_arm_gcc.S index 8e11645..c8b15e6 100644 --- a/stackman/platforms/switch_arm_gcc.S +++ b/stackman/platforms/switch_arm_gcc.S @@ -43,8 +43,10 @@ stackman_switch: blx r3 mov r2, r0 .syntax divided -@ 88 "../platforms/switch_arm_gcc.h" 1 +@ 97 "../platforms/switch_arm_gcc.h" 1 + sub r1, sp, r0 mov sp, r0 + sub r11, r11, r1 @ 0 "" 2 .arm .syntax unified diff --git a/stackman/platforms/switch_arm_gcc.h b/stackman/platforms/switch_arm_gcc.h index 5fd867c..b0f2e99 100644 --- a/stackman/platforms/switch_arm_gcc.h +++ b/stackman/platforms/switch_arm_gcc.h @@ -5,6 +5,13 @@ * */ +/* The checked-in assembly is armv5t soft-float and does not spill d8-d15. + * __ARM_PCS_VFP is the hard-float (gnueabihf) ABI. Softfp does not define it. + */ +#if defined(__ARM_PCS_VFP) +#error The checked-in ARM32 assembly does not spill d8-d15. Hard-float (__ARM_PCS_VFP) is not supported. +#endif + /* clang cannot perform inline assembly using specific __attr__ * instructions, and so it may use a base pointer and other * things. We must force it to use the pre-build assembler @@ -56,12 +63,10 @@ */ #define NV_REGISTERS "r4", "r5","r6", "r7", "r8", "r9", "r10" , "r11" -/* These are the floating point extension registers. Same applies, we must preserve them in - * case the calling function was doing any floating point logic. - * it may be optionally disabled to store these floating point registers by not - * defining them here. - * Note that we do not - * preserve the FPSCR and VPR registers since they have complex rules about preservation. +/* Inline assembly only. switch_arm_gcc.S is .fpu softvfp and does not + * spill these. A hard-float build is rejected above. + * FPSCR and VPR are not preserved; their preservation rules are more + * specific than a blanket spill. */ #ifndef __thumb__ #define CP_REGISTERS "d8","d9","d10","d11","d12","d13","d14","d15" @@ -84,8 +89,16 @@ void *STACKMAN_SWITCH_INASM_NAME(stackman_cb_t callback, void *context) __asm__ ("mov %[result], sp" : [result] "=r" (sp)); /* store stack */ sp = callback(context, STACKMAN_OP_SAVE, sp); - /* set stack pointer from sp using assembly */ - __asm__ ("mov sp, %[result]" : : [result] "r" (sp)); + /* Keep the caller's frame pointer attached to the new stack. + * r1 is free until the compiler assigns the RESTORE opcode. + * r11 is not an asm clobber: a clobber would make gcc save + * and restore it around this block and undo the delta. + */ + __asm__ ( + "sub r1, sp, %[result]\n\t" + "mov sp, %[result]\n\t" + "sub r11, r11, r1" + : : [result] "r" (sp) : "r1"); sp = callback(context, STACKMAN_OP_RESTORE, sp); /* restore registers */ return sp; From 0ee6216e0ae393656dfc6087557d32e8a7e1a854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 11:18:19 +0000 Subject: [PATCH 10/15] Remove the dropped Windows ARM32 switch. CHANGELOG 1.0.0 already dropped win_arm. The sources were still selected for _M_ARM, and the VS projects still built them. That assembly pushed an odd number of registers and then vpush'd at 4 mod 8. Delete the sources, the platform branch, the Debug|ARM project configurations, and the prebuilt lib/win_arm archive. Windows ARM64 is unchanged. --- docs/abi-register-audit.md | 34 ++--------- lib/win_arm/stackman.lib | Bin 4982 -> 0 bytes stackman/platforms/platform.h | 4 -- stackman/platforms/switch_arm64_msvc.asm | 1 - stackman/platforms/switch_arm_msvc.asm | 72 ----------------------- stackman/platforms/switch_arm_msvc.h | 8 --- vs2017/stackman.sln | 5 -- vs2017/stackman/stackman.vcxproj | 52 ---------------- vs2017/stackman/stackman.vcxproj.filters | 6 -- vs2017/test/test.vcxproj | 31 ---------- vs2019/stackman.sln | 5 -- vs2019/stackman/stackman.vcxproj | 52 ---------------- vs2019/stackman/stackman.vcxproj.filters | 6 -- vs2019/test/test.vcxproj | 31 ---------- vs2022/stackman.sln | 5 -- vs2022/stackman/stackman.vcxproj | 52 ---------------- vs2022/stackman/stackman.vcxproj.filters | 6 -- vs2022/test/test.vcxproj | 31 ---------- 18 files changed, 4 insertions(+), 397 deletions(-) delete mode 100644 lib/win_arm/stackman.lib delete mode 100644 stackman/platforms/switch_arm_msvc.asm delete mode 100644 stackman/platforms/switch_arm_msvc.h diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 7e072e0..57032f7 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -53,7 +53,9 @@ Per-platform comments, checked against the code: `lib/README.md` is about prebuilt archives, not the API or the register sets. Its platform list omits `riscv64` and the Darwin ABIs that the main README lists. `CHANGELOG.md` records Windows ARM32 as dropped in 1.0.0. -`platform.h` still selects `switch_arm_msvc.h` when `_M_ARM` is set. +The sources, the `_M_ARM` branch in `platform.h`, the Debug|ARM project +configurations, and the prebuilt `lib/win_arm` archive are gone. Windows +ARM64 stays. ## AArch64 @@ -122,7 +124,7 @@ not run it. | `switch_x86_gcc.S` | `ebx`, `esi`, `edi`, `ebp`, x87 control word | Integer set is complete. There is no callee-saved XMM on i386 SysV. `fstcw` stores the control word at `0x10(%esp)`, the pointer passed to the callback, and `fldcw` loads it after the restore callback. | | `switch_x86_msvc.asm` | `ebx`, `esi`, `edi`, `ebp`, x87 control word, plus the C wrapper saves `fs:[0]` | Integer set is complete for cdecl. `stackman_switch_raw` stores the control word in a slot below the callee-saved pushes, at the pointer passed to the callback. The C wrapper still restores `fs:[0]`. | | `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 not spilled, and `__ARM_PCS_VFP` is a compile error. The live `r11` is adjusted with the stack delta before `STACKMAN_OP_RESTORE`; the epilogue still pops the saved value. | -| `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. | +| Windows ARM32 | — | Removed. `CHANGELOG.md` dropped `win_arm` in 1.0.0. The old `vpush` ran at 4 mod 8, and `stackman_call` called with `sp` still 4 mod 8. The sources are deleted so that assembly cannot be 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. The optional `s0` frame delta is not applied: `s0` is not a frame chain there, and `stackman_call` uses it to hold the previous stack pointer. | ### RISC-V `stackman_call` @@ -152,34 +154,6 @@ spill the ZA array in this file. `FPMR`, `FFR`, and `ZT0` stay caller-saved. Skip the call on the Windows ARM64 file; this helper is the AAPCS64 one. This can wait until a supported target actually enables SME. -### Windows ARM32: delete it, or align `sp` before `vpush` - -`CHANGELOG.md` says 1.0.0 dropped `win_arm`. The sources are still wired -up: `switch_arm_msvc.asm`, `switch_arm_msvc.h`, the `_M_ARM` branch in -`platform.h`, and the `win_arm` output directories in the VS projects. -Preferred fix: delete that wiring so the file cannot be built. - -If the file stays, the entry `sp` is 8-byte aligned and -`push {r4-r10, fp, lr}` subtracts 36 bytes, so `vpush {q4-q7}` runs at -4 mod 8. Push a pad so the count is even, then pad `FPSCR` the same way, -or the `blx` is 4 mod 8 again: - -```asm -push {r3, r4, r5, r6, r7, r8, r9, r10, fp, lr} ; 40 bytes -add fp, sp, #32 ; saved fp is the 9th word -vpush {q4-q7} -vmrs r2, fpscr -push {r2, r3} ; fpscr plus a pad -``` - -Pop in reverse, with `pc` in place of `lr`. `r3` is a pad; it does not -have to be preserved beyond the pop. - -`stackman_call` has the same call-site bug. -`push {r4, fp, lr}` is 12 bytes. Push one more register -(`push {r3, r4, fp, lr}`) and point `fp` at the saved frame pointer with -`add fp, sp, #8`. - ## What the current tests cover `tests/test.c` already exercises a lot of the control path: diff --git a/lib/win_arm/stackman.lib b/lib/win_arm/stackman.lib deleted file mode 100644 index b45daad322ffab94960626d02199ef712a18370b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4982 zcmcH+TWs6b_0Y0gSB@QdowFh68dgbBra&=hSmCPj^u zoW%VJx)gh~MT>m|>3|JhkYNRi`~>-Eiy0V#0V|L->qmjL*t#J=wLt1M(LML@+;i@EUGhA!U(m(*gU&v>ad^CqYkS37NWagf9CHFd3jj4vz$gmh zf-2;BQBV}9u8b3MQJjaZY8kNA(Z`&q0>&wT$6HNxK;d6i{`%UQ)ZNXd0e{V=@Z=a5 zlV(bDT;=GzpsT!UEQ*Xaa~$fPTK1Y`XJ7Ta6-gUu^s;Oq5TNP6z&=amG3W0oVj(~d zZR|2JfWCeUv8H+QypaS$!2XWOk)!U(*pJR3@<)4N%|qadcy6wyJIjxCcs_PLU;dBl z>DJ4RM0?_RBE1S!TjH_Ax#5zx)j=U?kT+grESbfQJ z(>2mrZtWmDyAulThQ094izuUoIye05#L5lVk1<40MtA`s1ynaVLM+4yoE@+lvS|2N zpRcyAbFG;(jgE)?u)o`Zj{1~{G)EWoLeCk1Fo1Hno{lYDM7^-G-qW&+@f$?2$g(RMGeNc*eFRrlj`Azx!Y zkDx)@(A2EuK62%dd}c^R9#=(sH_`aI?P80Fe0F_gZNAg(04rbRi!kE+f)S_{T`z7p zfah-*s2pwVi^UV+L#b>k840<^GE>pXY|PzAO{Zex;jwteO%qk59-c*r{onOaf7OJr z4uEdKI(XE|e|v7MgLI)NtMZbZpQEFg`3i_kQG9xJdo^g8%qWuMZ4$ z?!4^oc@z)#3G(69v*D#_`?Gp&^2A%r{6?2-mER7Q!P(fBHZ`Ti*Vt=owj#fU9Sp{m z4Hipu-*)=R*_S(Q4$n7Q!M3V zjWb_-@qAIz3%YDb+*FjyYMO!@@H*kV{Y*d0`29h4keiY7+=ylrr-K0wKSa1_ijhuA zuvT_kIvIw;f7{tr1bw(x4s33RBF}IqFql2{P`(shslh+h& zPNO}C{Rq<@+LM!oxjY_C7LAaoDJ3;edyJeyd(t_CQ^*nHWAVtLVS?agNhW+b^M{k; z;fW+a5uVH@v&r;0GG#`y=?Oj)8|A}O;bbZtNyQI%3ICz-WHcR%BU;6VRg@ebF~|>| zZ!#N2&aoKk9L~_5*+Qs*ZyIze9$gHb5a$J*FPb=BRT)t$R36BAMb0BykaSfx40%!V zK)Hpg#GUp?Cowj8K?xCShf#JGJs=?A)RKhi6S$IeUDI``py|cX4@y!Ag(ck&AOqyQ zsFZS&`vHj;ii$cba<&#&vtoa*>cXXyvLXxmDQ+rLmA;-(y7y!-u$Mb5=X2T#gC3QO z2IpnDWW{Vm8J6++0zNOpawSnOuIq>`2~nFw)ND4Zb}Og*cpnTC z3x%&M2+CjDaW)-39?vGGvHrMzCdhcJ&My&HRcqk^r}u-^)12q))cU>Iw?V>hV%1hM z#LE2pB~5k1Cu`-~b`ES7`l%@GwF< z!XpUSi@+56rvv9rYhXS9Y*tRwP~HiS9oVp-WD21L!GW+1VXge^lRa;ZI=XNEL^(3} z%PR+W4KD5e?7IsCi&y?Kc9ms6uJ%l|hPJh-p?%pwowgd3H&uR<4mXkg9xS`oGTR4o7p=m z6b&gvR)uPvkg!##6lvlbM}7N7&gP&<1J^j7!{w%p4$rUG+VK1)%?!_P*uLPSbsCoc E0Y;T31^@s6 diff --git a/stackman/platforms/platform.h b/stackman/platforms/platform.h index 90b0dd1..33b3917 100644 --- a/stackman/platforms/platform.h +++ b/stackman/platforms/platform.h @@ -37,10 +37,6 @@ #include "switch_x64_msvc.h" /* MS Visual Studio on X64 */ #define _STACKMAN_PLATFORM x64_msvc #define _STACKMAN_ABI win_x64 -#elif defined(_M_ARM) -#include "switch_arm_msvc.h" /* MS Visual Studio on ARM */ -#define _STACKMAN_PLATFORM arm_msvc -#define _STACKMAN_ABI win_arm #elif defined(_M_ARM64) #include "switch_arm64_msvc.h" /* MS Visual Studio on ARM */ #define _STACKMAN_PLATFORM arm64_msvc diff --git a/stackman/platforms/switch_arm64_msvc.asm b/stackman/platforms/switch_arm64_msvc.asm index 7608653..0745540 100644 --- a/stackman/platforms/switch_arm64_msvc.asm +++ b/stackman/platforms/switch_arm64_msvc.asm @@ -2,7 +2,6 @@ ; this code is adopted from the switch_aarch64_gcc.S ; which again was generated from switch_arm_gcc.h -; This code is also modelled on the hand-customized switch_arm_msvc.asm ; see https://docs.microsoft.com/en-us/cpp/build/arm64-windows-abi-conventions?view=msvc-160 ;ARM64 diff --git a/stackman/platforms/switch_arm_msvc.asm b/stackman/platforms/switch_arm_msvc.asm deleted file mode 100644 index 5caca3c..0000000 --- a/stackman/platforms/switch_arm_msvc.asm +++ /dev/null @@ -1,72 +0,0 @@ - - -; this code is adopted from the switch_arm_gcc.S -; which again was generated from switch_arm_gcc.h - - THUMB - - EXPORT |stackman_switch| - ALIGN -|stackman_switch| PROC - push {r4 - r10, fp, lr} ;9 x 4bytes - add fp, sp, #28 ; (4x7) fp must point to the previous fp on the stack - - ; also push floating point registers and fpscr (status&control register) - vpush {q4-q7} - vmrs r2,fpscr - push {r2} ;5 x 4bytes - ; total of 14 words pushed, satisfying 8 byte stack alignment. - - ; args are r0=callback, r1=context - ; shuffle calling arguments into r0, r1 and r2 - ; for the call to the callback. - mov r3, r0 - mov r0, r1 ;context - mov r2, sp ;stack pointer - mov r1, #0 ;operation - mov r5, r0 ;store these a bit - mov r4, r3 - blx r3 ;first callback - mov r2, r0 ;store new stackpointer in register, for next cb, - ; switch stack pointer, maintaining fp offset - sub r1, sp, r0 - mov sp, r0 ;switch stack pointer - sub fp, fp, r1 - - mov r1, #1 ;operation - mov r0, r5 ;context - blx r4 ;second callback - ; pop and return - pop {r2} - vmsr fpscr, r2 - vpop {q4, q5, q6, q7} - pop {r4-r9, r10, fp, pc} - ENDP - - EXPORT |stackman_call| - ALIGN -|stackman_call| PROC - ; args are r0 = callback, r1 = context, r2=stack - push {r4, fp, lr} - add fp, sp, #4 ;fp must always point to the last fp (r11) - mov r3, r0 - mov r0, r1 ;context - mov r1, r2 ;stack - ; store current sp in nv register - mov r4, sp - mov r2, sp ; old stack - - ; change stack if non-zero - cmp r1, #0 - beq nullptr - mov sp, r1 -nullptr - mov r1, #2 ; callback opcode - blx r3 ; call callback, with context, opcode, old stack - ; restore stack (could do: sub sp, fp #12) - mov sp, r4 - ; return - pop {r4, fp, pc} - ENDP - - END diff --git a/stackman/platforms/switch_arm_msvc.h b/stackman/platforms/switch_arm_msvc.h deleted file mode 100644 index 02025c6..0000000 --- a/stackman/platforms/switch_arm_msvc.h +++ /dev/null @@ -1,8 +0,0 @@ -/* The actual stack saving function, which just stores the stack, - * this declared in an .asm file - */ -#ifndef STACKMAN_ASSEMBLY_SRC -#define STACKMAN_ASSEMBLY_SRC switch_arm_msvc.asm -#define STACKMAN_HAVE_CALL 1 -#define STACKMAN_STACK_ALIGN 8 -#endif diff --git a/vs2017/stackman.sln b/vs2017/stackman.sln index 6dff39b..3a11a17 100644 --- a/vs2017/stackman.sln +++ b/vs2017/stackman.sln @@ -12,22 +12,17 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "stackman", "stackman\stackm EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|ARM = Debug|ARM Debug|ARM64 = Debug|ARM64 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|ARM.ActiveCfg = Debug|ARM - {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|ARM.Build.0 = Debug|ARM {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|ARM64.Build.0 = Debug|ARM64 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|x64.ActiveCfg = Debug|x64 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|x64.Build.0 = Debug|x64 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|x86.ActiveCfg = Debug|Win32 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|x86.Build.0 = Debug|Win32 - {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|ARM.ActiveCfg = Debug|ARM - {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|ARM.Build.0 = Debug|ARM {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|ARM64.ActiveCfg = Debug|ARM64 {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|ARM64.Build.0 = Debug|ARM64 {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|x64.ActiveCfg = Debug|x64 diff --git a/vs2017/stackman/stackman.vcxproj b/vs2017/stackman/stackman.vcxproj index be7106e..76a073a 100644 --- a/vs2017/stackman/stackman.vcxproj +++ b/vs2017/stackman/stackman.vcxproj @@ -1,10 +1,6 @@ - - Debug - ARM - Debug ARM64 @@ -39,12 +35,6 @@ v141 Unicode - - StaticLibrary - true - v141 - Unicode - StaticLibrary true @@ -64,9 +54,6 @@ - - - @@ -80,10 +67,6 @@ true $(SolutionDir)\..\lib\win_x64\ - - true - $(SolutionDir)\..\lib\win_arm\ - true $(SolutionDir)\..\lib\win_arm64\ @@ -128,26 +111,6 @@ /Brepro %(AdditionalOptions) - - - Level3 - true - _DEBUG;_LIB;%(PreprocessorDefinitions) - true - /Zl %(AdditionalOptions) - NotUsing - pch.h - OldStyle - - - - - true - - - /Brepro %(AdditionalOptions) - - Level3 @@ -171,7 +134,6 @@ - @@ -185,44 +147,30 @@ AssemblyCode - AssemblyCode AssemblyCode false - false false Default - Default Default false - false false true - true true true - - false - true - true - true - Document - true true true true - true true true - true true diff --git a/vs2017/stackman/stackman.vcxproj.filters b/vs2017/stackman/stackman.vcxproj.filters index 9f57a1f..d4051af 100644 --- a/vs2017/stackman/stackman.vcxproj.filters +++ b/vs2017/stackman/stackman.vcxproj.filters @@ -33,9 +33,6 @@ Header Files - - Header Files - Header Files @@ -57,9 +54,6 @@ - - Source Files - Source Files diff --git a/vs2017/test/test.vcxproj b/vs2017/test/test.vcxproj index 9d14c4b..283f20c 100644 --- a/vs2017/test/test.vcxproj +++ b/vs2017/test/test.vcxproj @@ -1,10 +1,6 @@ - - Debug - ARM - Debug ARM64 @@ -38,12 +34,6 @@ v141 Unicode - - Application - true - v141 - Unicode - Application true @@ -61,9 +51,6 @@ - - - @@ -76,9 +63,6 @@ true - - true - true @@ -113,21 +97,6 @@ ..\..\lib\win_x64;%(AdditionalLibraryDirectories) - - - Level3 - true - _DEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - ..\..\stackman - - - Console - true - stackman.lib;%(AdditionalDependencies) - ..\..\lib\win_arm;%(AdditionalLibraryDirectories) - - Level3 diff --git a/vs2019/stackman.sln b/vs2019/stackman.sln index 6dff39b..3a11a17 100644 --- a/vs2019/stackman.sln +++ b/vs2019/stackman.sln @@ -12,22 +12,17 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "stackman", "stackman\stackm EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|ARM = Debug|ARM Debug|ARM64 = Debug|ARM64 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|ARM.ActiveCfg = Debug|ARM - {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|ARM.Build.0 = Debug|ARM {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|ARM64.Build.0 = Debug|ARM64 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|x64.ActiveCfg = Debug|x64 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|x64.Build.0 = Debug|x64 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|x86.ActiveCfg = Debug|Win32 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|x86.Build.0 = Debug|Win32 - {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|ARM.ActiveCfg = Debug|ARM - {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|ARM.Build.0 = Debug|ARM {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|ARM64.ActiveCfg = Debug|ARM64 {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|ARM64.Build.0 = Debug|ARM64 {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|x64.ActiveCfg = Debug|x64 diff --git a/vs2019/stackman/stackman.vcxproj b/vs2019/stackman/stackman.vcxproj index cfe06e8..fc969f6 100644 --- a/vs2019/stackman/stackman.vcxproj +++ b/vs2019/stackman/stackman.vcxproj @@ -1,10 +1,6 @@ - - Debug - ARM - Debug ARM64 @@ -39,12 +35,6 @@ v142 Unicode - - StaticLibrary - true - v142 - Unicode - StaticLibrary true @@ -64,9 +54,6 @@ - - - @@ -80,10 +67,6 @@ true $(SolutionDir)\..\lib\win_x64\ - - true - $(SolutionDir)\..\lib\win_arm\ - true $(SolutionDir)\..\lib\win_arm64\ @@ -128,26 +111,6 @@ /Brepro %(AdditionalOptions) - - - Level3 - true - _DEBUG;_LIB;%(PreprocessorDefinitions) - true - /Zl %(AdditionalOptions) - NotUsing - pch.h - OldStyle - - - - - true - - - /Brepro %(AdditionalOptions) - - Level3 @@ -171,7 +134,6 @@ - @@ -185,44 +147,30 @@ AssemblyCode - AssemblyCode AssemblyCode false - false false Default - Default Default false - false false true - true false true - - false - true - true - true - Document - true true true true - true true true - true true diff --git a/vs2019/stackman/stackman.vcxproj.filters b/vs2019/stackman/stackman.vcxproj.filters index 9f57a1f..d4051af 100644 --- a/vs2019/stackman/stackman.vcxproj.filters +++ b/vs2019/stackman/stackman.vcxproj.filters @@ -33,9 +33,6 @@ Header Files - - Header Files - Header Files @@ -57,9 +54,6 @@ - - Source Files - Source Files diff --git a/vs2019/test/test.vcxproj b/vs2019/test/test.vcxproj index 93dc2cf..54109b3 100644 --- a/vs2019/test/test.vcxproj +++ b/vs2019/test/test.vcxproj @@ -1,10 +1,6 @@ - - Debug - ARM - Debug ARM64 @@ -38,12 +34,6 @@ v142 Unicode - - Application - true - v142 - Unicode - Application true @@ -61,9 +51,6 @@ - - - @@ -76,9 +63,6 @@ true - - true - true @@ -113,21 +97,6 @@ ..\..\lib\win_x64;%(AdditionalLibraryDirectories) - - - Level3 - true - _DEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - ..\..\stackman - - - Console - true - stackman.lib;%(AdditionalDependencies) - ..\..\lib\win_arm;%(AdditionalLibraryDirectories) - - Level3 diff --git a/vs2022/stackman.sln b/vs2022/stackman.sln index 6dff39b..3a11a17 100644 --- a/vs2022/stackman.sln +++ b/vs2022/stackman.sln @@ -12,22 +12,17 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "stackman", "stackman\stackm EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|ARM = Debug|ARM Debug|ARM64 = Debug|ARM64 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|ARM.ActiveCfg = Debug|ARM - {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|ARM.Build.0 = Debug|ARM {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|ARM64.Build.0 = Debug|ARM64 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|x64.ActiveCfg = Debug|x64 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|x64.Build.0 = Debug|x64 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|x86.ActiveCfg = Debug|Win32 {CAFDBD3E-9D0D-4E90-BB6D-6C2A19F5EF53}.Debug|x86.Build.0 = Debug|Win32 - {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|ARM.ActiveCfg = Debug|ARM - {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|ARM.Build.0 = Debug|ARM {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|ARM64.ActiveCfg = Debug|ARM64 {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|ARM64.Build.0 = Debug|ARM64 {BF7D0638-AC4F-4206-B426-66CDDD468281}.Debug|x64.ActiveCfg = Debug|x64 diff --git a/vs2022/stackman/stackman.vcxproj b/vs2022/stackman/stackman.vcxproj index 32dc9f0..9b37f4d 100644 --- a/vs2022/stackman/stackman.vcxproj +++ b/vs2022/stackman/stackman.vcxproj @@ -1,10 +1,6 @@ - - Debug - ARM - Debug ARM64 @@ -39,12 +35,6 @@ v143 Unicode - - StaticLibrary - true - v143 - Unicode - StaticLibrary true @@ -64,9 +54,6 @@ - - - @@ -80,10 +67,6 @@ true $(SolutionDir)\..\lib\win_x64\ - - true - $(SolutionDir)\..\lib\win_arm\ - true $(SolutionDir)\..\lib\win_arm64\ @@ -128,26 +111,6 @@ /Brepro %(AdditionalOptions) - - - Level3 - true - _DEBUG;_LIB;%(PreprocessorDefinitions) - true - /Zl %(AdditionalOptions) - NotUsing - pch.h - OldStyle - - - - - true - - - /Brepro %(AdditionalOptions) - - Level3 @@ -171,7 +134,6 @@ - @@ -185,44 +147,30 @@ AssemblyCode - AssemblyCode AssemblyCode false - false false Default - Default Default false - false false true - true false true - - false - true - true - true - Document - true true true true - true true true - true true diff --git a/vs2022/stackman/stackman.vcxproj.filters b/vs2022/stackman/stackman.vcxproj.filters index 9f57a1f..d4051af 100644 --- a/vs2022/stackman/stackman.vcxproj.filters +++ b/vs2022/stackman/stackman.vcxproj.filters @@ -33,9 +33,6 @@ Header Files - - Header Files - Header Files @@ -57,9 +54,6 @@ - - Source Files - Source Files diff --git a/vs2022/test/test.vcxproj b/vs2022/test/test.vcxproj index 266eb72..10cbf33 100644 --- a/vs2022/test/test.vcxproj +++ b/vs2022/test/test.vcxproj @@ -1,10 +1,6 @@ - - Debug - ARM - Debug ARM64 @@ -38,12 +34,6 @@ v143 Unicode - - Application - true - v143 - Unicode - Application true @@ -61,9 +51,6 @@ - - - @@ -76,9 +63,6 @@ true - - true - true @@ -113,21 +97,6 @@ ..\..\lib\win_x64;%(AdditionalLibraryDirectories) - - - Level3 - true - _DEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - ..\..\stackman - - - Console - true - stackman.lib;%(AdditionalDependencies) - ..\..\lib\win_arm;%(AdditionalLibraryDirectories) - - Level3 From 14063c27e0ce48c4cadb764cd49dbd7f8825eb40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 13:51:51 +0000 Subject: [PATCH 11/15] update ZA information --- docs/abi-register-audit.md | 387 ++++++++++++++++++++++++++++++++----- 1 file changed, 335 insertions(+), 52 deletions(-) diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 57032f7..4460873 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -16,6 +16,9 @@ Sources: AAPCS64 (2025Q4 / 2026Q3), Microsoft ARM64 and ARM32 ABI notes, Microsoft x64 calling convention, System V i386 and AMD64 ABIs, RISC-V psABI LP64D. +The findings below are the audit as it stood when the gaps were found. Each +section keeps that text. **Applied fix** records what was changed afterward. + ## Existing documentation There is no file that lists, for each ABI, which registers `stackman_switch` @@ -38,24 +41,30 @@ The API is described in two places: `switch_riscv64_gcc.S` now follows it: a null stack is left unchanged, and the callback receives the previous stack pointer. -Per-platform comments, checked against the code: +Per-platform comments, checked against the code at audit time: | Place | Claim | Matches the code | | --- | --- | --- | | `switch_x86_64_gcc.h` | `rbx`, `rbp`, `r12`–`r15`, x87 control word, MXCSR | Yes. The comment used to say "mmx"; the instruction is `stmxcsr`. | -| `switch_x86_gcc.h` | `ebx`, `esi`, `edi`, `ebp` via the frame, x87 control word | Yes. The control word is stored with `fstcw` at the pointer passed to the callback. | +| `switch_x86_gcc.h` | `ebx`, `esi`, `edi`, and `ebp` via the frame | Yes for what it saves. It does not mention the x87 control word. | | `switch_aarch64_gcc.h` | `x19`–`x28`, `x29` via the frame, `d8`–`d15` | Yes for the base AAPCS64 set. Silent on `x18` and SME. | -| `switch_arm_gcc.h` | `r4`–`r11`, and `d8`–`d15` unless Thumb. `FPSCR` is not saved. | The integer list matches `switch_arm_gcc.S`. `CP_REGISTERS` is the inline clobber only; that `.S` is soft-float and does not save `d8`–`d15`. `__ARM_PCS_VFP` is a compile error. | +| `switch_arm_gcc.h` | `r4`–`r11`, and `d8`–`d15` unless Thumb. `FPSCR` is not saved. | The integer list matches `switch_arm_gcc.S`. That `.S` is soft-float and does not save `d8`–`d15`, so `CP_REGISTERS` describes the inline path only. | | `switch_riscv64_gcc.S` | `s0`–`s11`, `fs0`–`fs11`, `ra` for `stackman_switch` | Yes for that function. `stackman_call` skips a null stack and passes the previous stack pointer. | -| `switch_x64_msvc.asm` | non-volatile GPRs, `xmm6`–`xmm15`, and MXCSR | Yes. The control bits of MXCSR are saved with the xmm frame. Upper ymm/zmm bits stay volatile. | -| `switch_arm64_msvc.asm` | `x18`–`x28`, `d8`–`d15`, `FPCR` are pushed | The spill matches the comment. `x18` is spilled and restored, and neither routine writes the live register. The callback is held in `x20`. `stackman_call` holds the old stack pointer in `x19`. | +| `switch_x64_msvc.asm` | "all registers that the x64 ABI specifies as non-volatile", plus xmm | The GPRs and `xmm6`–`xmm15` match. MXCSR[6:15] is non-volatile on Windows and is not saved, so the comment overclaims. | +| `switch_arm64_msvc.asm` | `x18`–`x28`, `d8`–`d15`, `FPCR` are pushed | The spill matches the comment. The routine then uses `x18` as a scratch register, which the Windows ABI does not allow. | `lib/README.md` is about prebuilt archives, not the API or the register sets. Its platform list omits `riscv64` and the Darwin ABIs that the main README lists. `CHANGELOG.md` records Windows ARM32 as dropped in 1.0.0. -The sources, the `_M_ARM` branch in `platform.h`, the Debug|ARM project -configurations, and the prebuilt `lib/win_arm` archive are gone. Windows -ARM64 stays. +`platform.h` still selects `switch_arm_msvc.h` when `_M_ARM` is set. + +**Applied fix.** The i386 header comment now names the x87 control word. +The Windows x64 comment names the non-volatile GPRs, `xmm6`–`xmm15`, and +MXCSR. The ARM32 header rejects `__ARM_PCS_VFP` and says `CP_REGISTERS` is +the inline clobber only. The Windows ARM64 routines no longer write live +`x18`. The `_M_ARM` branch, the Debug|ARM project configurations, and the +prebuilt `lib/win_arm` archive were removed. Windows ARM64 stays. The +`lib/README.md` platform list was left as it was. ## AArch64 @@ -83,49 +92,103 @@ caller-saved. never writes `x18`. `stackman_call` spills `x29`, `x30`, and `x19`, and holds the old stack pointer in `x19`. -`stackman_switch` applies the stack delta to the live `x29` before -`STACKMAN_OP_RESTORE` (`sub x1, sp, x0` / `mov sp, x0` / `sub x29, x29, x1`). -The same three instructions are in `switch_aarch64_gcc.h`, so regenerating -the `.S` from `gen_asm.c` keeps them. The epilogue still reloads the saved -`x29`, so the caller receives the stacked value. The adjustment matters only -during `STACKMAN_OP_RESTORE`. A callback that relocates a stack still has to -rewrite saved frame-pointer slots in the copied bytes. - -SME lazy save is not called. AAPCS64 says `setjmp` / `longjmp` call -`__arm_za_disable` so the ZA buffer (`TPIDR2_EL0`) is not left aimed at the -wrong stack. That can wait until a supported target enables SME. +Two gaps that are not missing base registers: + +- After `mov sp, x0` the live `x29` is left pointing at the old stack. The + Windows ARM64 and SysV x86-64 routines add the same delta to the frame + pointer. The epilogue still reloads the saved `x29`, so the caller receives + the stacked value. The frame chain is wrong only during + `STACKMAN_OP_RESTORE`. +- A stack switch is an asynchronous transfer of control. AAPCS64 says + `setjmp` / `longjmp` call `__arm_za_disable` so the SME lazy-save buffer + (`TPIDR2_EL0`) is not left aimed at the wrong stack. This file does not. + +**Applied fix.** `stackman_switch` applies the stack delta to the live +`x29` before `STACKMAN_OP_RESTORE` (`sub x1, sp, x0` / `mov sp, x0` / +`sub x29, x29, x1`). The same three instructions are in +`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. ### `switch_arm64_msvc.asm` (Windows `win_arm64`) The spill set matches the Windows document: `x19`–`x28`, `x29`, `x30`, -`d8`–`d15`, `FPCR`, and `x18`. `x18` is the TEB for the whole time user -code runs. Both routines spill it and leave the live register alone. +`d8`–`d15`, `FPCR`, and `x18`. The bug is the live value of `x18` during the +callbacks. + +`stackman_switch` stores the callback in `x18` and calls through it: -`stackman_switch` keeps the context in `x19` and the callback in `x20`. -The first call is `blr x3` after `x20` has been loaded from `x3`, because -the callback clobbers `x3`. The second call is `blr x20`. The `x18` store -and reload stay, so a restored context gets the value it spilled. +```asm +mov x19, x0 +mov x18, x3 +blr x3 ; first callback, x18 is no longer the TEB +... +blr x18 ; second callback +``` -`stackman_call` spills `x19` at `[sp, #16]` and keeps the old stack pointer -there. It does not read or write `x18`. +`stackman_call` parks the old stack pointer in `x18` across `blr x3`. TLS, +the stack limit, `__chkstk`, and any Win32 call read the TEB through `x18`. +The epilogue writes the saved value back, so the original caller is repaired +only when the callback returns. -The file adjusts `fp` across the switch, as `switch_aarch64_gcc.S` does for `x29`. +Fix: keep `x18` unchanged. `stackman_switch` already spills `x19`–`x28`; use +one of those for the callback. `stackman_call` should spill some other +non-volatile (for example `x19`) and keep the old stack pointer there. + +The Windows file adjusts `fp` across the switch, which the SysV file does not. It has no SEH unwind opcodes. The x64 MASM uses `NESTED_ENTRY` and `.pushreg` / `save_xmm128`. That is an unwinding gap, separate from the -register values. The Windows ARM64 test job compiles this file and does -not run it. +register values. + +**Applied fix.** `stackman_switch` keeps the context in `x19` and the +callback in `x20`. The first call is `blr x3` after `x20` has been loaded +from `x3`. The second call is `blr x20`. The `x18` store and reload stay, +so a restored context gets the value it spilled. `stackman_call` spills +`x19` at `[sp, #16]` and keeps the old stack pointer there. The SysV +AArch64 file now adjusts `x29` the same way this file adjusts `fp`. SEH +unwind codes were left unchanged. The Windows ARM64 test job compiles this +file and does not run it. ## Other platforms | File | What it spills | Result | | --- | --- | --- | | `switch_x86_64_gcc.S` | `rbx`, `rbp`, `r12`–`r15`, x87 control word, MXCSR | Complete for SysV AMD64. XMM registers are volatile there. The frame pointer is adjusted with the stack delta. | -| `switch_x64_msvc.asm` | `rbx`, `rbp`, `rdi`, `rsi`, `r12`–`r15`, `xmm6`–`xmm15`, MXCSR | Those registers match the Windows x64 ABI. MXCSR is stored at `[rsp+160]`, the pad in the 168-byte xmm frame, and loaded back before that frame is released. `stackman_call` does not spill it. | -| `switch_x86_gcc.S` | `ebx`, `esi`, `edi`, `ebp`, x87 control word | Integer set is complete. There is no callee-saved XMM on i386 SysV. `fstcw` stores the control word at `0x10(%esp)`, the pointer passed to the callback, and `fldcw` loads it after the restore callback. | -| `switch_x86_msvc.asm` | `ebx`, `esi`, `edi`, `ebp`, x87 control word, plus the C wrapper saves `fs:[0]` | Integer set is complete for cdecl. `stackman_switch_raw` stores the control word in a slot below the callee-saved pushes, at the pointer passed to the callback. The C wrapper still restores `fs:[0]`. | -| `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 not spilled, and `__ARM_PCS_VFP` is a compile error. The live `r11` is adjusted with the stack delta before `STACKMAN_OP_RESTORE`; the epilogue still pops the saved value. | -| Windows ARM32 | — | Removed. `CHANGELOG.md` dropped `win_arm` in 1.0.0. The old `vpush` ran at 4 mod 8, and `stackman_call` called with `sp` still 4 mod 8. The sources are deleted so that assembly cannot be 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. The optional `s0` frame delta is not applied: `s0` is not a frame chain there, and `stackman_call` uses it to hold the previous stack pointer. | +| `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. `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. | + +**Applied fix.** + +- `switch_x86_64_gcc.S` was already complete. Nothing was changed. +- `switch_x64_msvc.asm` stores MXCSR at `[rsp+160]`, the pad in the + 168-byte xmm frame, with `stmxcsr` after `alloc_stack` and `ldmxcsr` + after the GPR pops. `stackman_call` still does not spill MXCSR, because + it returns to the same caller. +- `switch_x86_gcc.S` and the inline block in `switch_x86_gcc.h` store the + x87 control word with `fstcw` at `0x10(%esp)`, the pointer passed to the + callback, and reload it with `fldcw` after the restore callback. `fstcw` + is the same instruction `switch_x86_64_gcc.S` uses. XMM registers stay + unsaved. +- `switch_x86_msvc.asm` stores the control word in `stackman_switch_raw`, + in a slot allocated with `ebx`, `esi`, and `edi`, at the pointer passed + to the callback. The C wrapper still restores `fs:[0]`. +- `switch_arm_gcc.h` is a compile error when `__ARM_PCS_VFP` is set. + `d8`–`d15` were not added. `CP_REGISTERS` is marked as the inline-only + clobber. `stackman_switch` applies the stack delta to live `r11` + (`sub r1, sp, r0` / `mov sp, r0` / `sub r11, r11, r1`) in the `.S` and + in the header. The epilogue still pops the saved `r11`. +- 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 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` @@ -133,10 +196,11 @@ The register spill in `stackman_call` (`ra` and `s0`) is enough for the registers that routine writes. It used to switch `sp` even when the requested stack was null, and it passed that new pointer as the third argument. `test_04` then accepted `passed == stack2`, and `test_05` faulted -in the callback prologue. It now matches the other ports: a null stack is -left in place, and the callback receives the previous stack pointer (`s0`, -captured after the 16-byte frame allocation). The return value stays in -`a0`. +in the callback prologue. + +**Applied fix.** It now matches the other ports: a null stack is left in +place, and the callback receives the previous stack pointer (`s0`, captured +after the 16-byte frame allocation). The return value stays in `a0`. ## Suggested fixes @@ -144,6 +208,73 @@ Each item is a local edit. The spill lists that already match the ABI stay as they are. `stackman_call` only needs a new spill when the fix starts using a callee-saved register it does not already save. +The steps under each heading are the ones the audit proposed. The applied +fix at the end of the heading says what landed. + +### Windows ARM64: stop writing `x18` + +File: `stackman/platforms/switch_arm64_msvc.asm`. + +`stackman_switch` already spills `x19`–`x28` before it assigns them. Keep +the context in `x19`. Keep the callback in `x20` instead of `x18`, and make +the second call `blr x20`. The first call can stay `blr x3` only if `x20` +was loaded from `x3` first; the callback clobbers `x3`. Leave the `x18` +store and reload in place so a restored context gets the value it spilled, +and do not write `x18` between them. + +`stackman_call` should spill `x19` in the slot that now holds `x18` +(`str`/`ldr` at `[sp, #16]`), keep the old stack pointer in `x19`, and +delete every `x18` access. The 32-byte frame is already large enough. + +Unwind data is a separate change. Describe the prologue with the ARM64 +counterparts of the `NESTED_ENTRY` / `save_xmm128` macros in +`switch_x64_msvc.asm`, one unwind code per store, so `RtlUnwind` can find +`x19`–`x30`, `d8`–`d15`, and the `FPCR` slot. The Windows ARM64 test job +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. + +### AArch64 SysV and Apple: keep `x29` attached to the new stack + +File: `stackman/platforms/switch_aarch64_gcc.S`, in `stackman_switch`, +immediately after the first `blr` and before `mov w1, 1`. + +`x0` is the new stack pointer. `x19` is the callback and `x20` is the +context. `x1` is free until it becomes the opcode: + +```asm +sub x1, sp, x0 +mov sp, x0 +sub x29, x29, x1 +``` + +That is the same delta the Windows ARM64 file applies to `fp`. The epilogue +still reloads `x29` from the stack, so the caller receives the saved value, +not this adjusted one. The adjustment matters only while +`STACKMAN_OP_RESTORE` runs. A callback that relocates a stack still has to +rewrite saved frame-pointer slots in the copied bytes; this edit does not do +that. + +The checked-in `.S` is what default builds use (`STACKMAN_INLINE_ASM` is +unset). The inline sequence in `switch_aarch64_gcc.h` is the generator. Put +the same three instructions in that header if the `.S` is regenerated from +`gen_asm.c`, or the next regeneration will drop the delta. + +The same three-instruction pattern applies to `switch_arm_gcc.S` +(`sub r1, sp, r0` / `mov sp, r0` / `sub r11, r11, r1`) and to +`switch_riscv64_gcc.S` (`sub t0, sp, a0` before `mv sp, a0`, then add that +delta to `s0` only when `s0` is the live frame pointer). On RISC-V `s0` is +just a saved register in `stackman_switch`, not a frame chain, so the +integer spill is already complete and the delta is optional. `stackman_call` +uses `s0` to hold the previous stack pointer across the callback, so do not +adjust it there. + +**Applied fix.** The `x29` delta is in `switch_aarch64_gcc.S` and in +`switch_aarch64_gcc.h`. The `r11` delta is in `switch_arm_gcc.S` and in +`switch_arm_gcc.h`. The RISC-V `s0` delta was left unchanged. + ### AArch64 SME lazy save On entry to `stackman_switch`, before `sp` changes, call `__arm_za_disable` @@ -154,6 +285,151 @@ spill the ZA array in this file. `FPMR`, `FFR`, and `ZT0` stay caller-saved. Skip the call on the Windows ARM64 file; this helper is the AAPCS64 one. This can wait until a supported target actually enables SME. +**Applied fix.** None. `__arm_za_disable` is the wrong tool for this +switch. See Roadmap. + +### Windows x64: spill MXCSR with the xmm frame + +File: `stackman/platforms/switch_x64_msvc.asm`, `stackman_switch` only. + +`alloc_stack 168` covers `xmm6`–`xmm15` (160 bytes) plus an 8-byte pad at +`[rsp+160]`. After `alloc_stack` and before the `push_reg`s, `stmxcsr` +that dword. After the epilogue pops the GPRs and before `add rsp, 168`, +`ldmxcsr` the same slot. Restoring the whole register also restores the +volatile status flags, which is what `switch_x86_64_gcc.S` already does. +`stackman_call` does not need this: it returns to the same caller, and a +conforming callback restores MXCSR itself. + +Narrow the comment that says every non-volatile register is saved, once +MXCSR is included, or the comment stays ahead of the code. + +**Applied fix.** MXCSR is spilled and restored at `[rsp+160]` as described. +The comment now names the non-volatile GPRs, `xmm6`–`xmm15`, and MXCSR. + +### i386: spill the x87 control word + +The integer spills are complete. The gap is `fnstcw` / `fldcw` around the +two callbacks in `stackman_switch`, stored inside the frame that the +callback is asked to copy. + +- `switch_x86_gcc.S` and the inline block in `switch_x86_gcc.h` must + change together. The `.S` is a copy of the header's asm string. The + `subl $0x1c, %esp` frame has unused bytes at `0x10(%esp)`, `0x14(%esp)`, + and `0x18(%esp)`, and `0x10(%esp)` is the pointer passed to the callback. + `fnstcw` a word at that pointer before the first `call`, and `fldcw` it + after the second `call`, before `addl $0x1c, %esp`. After + `leal -0x10(%eax), %esp` the same word is at `0x10(%esp)` on the new + stack. Regenerating with `gcc -S -m32 -fcf-protection=none` from + `gen_asm.c` is the project's usual way to refresh the `.S`. +- `switch_x86_msvc.asm`: `fnstcw` / `fldcw` in `stackman_switch_raw`, in a + slot allocated with `ebx` / `esi` / `edi`, so the word is on the stack + image rather than in the C wrapper's `seh_state` local. The wrapper + already restores `fs:[0]` and should keep doing that. + +XMM registers stay unsaved: they are volatile for SysV i386 and for +Windows cdecl. `__vectorcall` is not the convention this library uses. + +**Applied fix.** Both gcc files and `stackman_switch_raw` store and reload +the control word in the frame the callback is asked to copy. The +instruction is `fstcw` / `fldcw`, matching `switch_x86_64_gcc.S`. The +wrapper still restores `fs:[0]`. + +### ARM32 soft-float: refuse a hard-float build + +`switch_arm_gcc.S` is `.arch armv5t` and `.fpu softvfp`, which matches +`arm-linux-gnueabi`. Do not add `d8`–`d15` until there is an `armhf` ABI. +Do stop a hard-float compile from silently using this file. In +`switch_arm_gcc.h`, `#error` when `__ARM_PCS_VFP` is set, with a line that +says the checked-in assembly does not spill `d8`–`d15`. Leave +`CP_REGISTERS` as the inline-only clobber, or drop it, so it cannot be read +as a description of `switch_arm_gcc.S`. + +The live `r11` adjustment in the AArch64 section above is optional here. +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 as +well. `d8`–`d15` were not added. A separate hard-float port is on the +roadmap. + +### Windows ARM32: delete it, or align `sp` before `vpush` + +`CHANGELOG.md` says 1.0.0 dropped `win_arm`. The sources are still wired +up: `switch_arm_msvc.asm`, `switch_arm_msvc.h`, the `_M_ARM` branch in +`platform.h`, and the `win_arm` output directories in the VS projects. +Preferred fix: delete that wiring so the file cannot be built. + +If the file stays, the entry `sp` is 8-byte aligned and +`push {r4-r10, fp, lr}` subtracts 36 bytes, so `vpush {q4-q7}` runs at +4 mod 8. Push a pad so the count is even, then pad `FPSCR` the same way, +or the `blx` is 4 mod 8 again: + +```asm +push {r3, r4, r5, r6, r7, r8, r9, r10, fp, lr} ; 40 bytes +add fp, sp, #32 ; saved fp is the 9th word +vpush {q4-q7} +vmrs r2, fpscr +push {r2, r3} ; fpscr plus a pad +``` + +Pop in reverse, with `pc` in place of `lr`. `r3` is a pad; it does not +have to be preserved beyond the pop. + +`stackman_call` has the same call-site bug. +`push {r4, fp, lr}` is 12 bytes. Push one more register +(`push {r3, r4, fp, lr}`) and point `fp` at the saved frame pointer with +`add fp, sp, #8`. + +**Applied fix.** The preferred option was taken. The sources, the +`_M_ARM` branch, the Debug|ARM configurations in vs2017, vs2019, and +vs2022, and `lib/win_arm/stackman.lib` were deleted. The alignment +alternative was not used. + +## Roadmap + +Two items from the audit are still open. + +### ARM32 hard-float + +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 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 + +SME (Scalable Matrix Extension) adds ZA, a matrix accumulator that is much +larger than the general-purpose and SIMD registers. A function that has ZA +live can avoid copying it on every call by leaving it dormant: +`TPIDR2_EL0` points at a save buffer on that function's stack, and a +normal callee leaves the buffer alone. `stackman_switch` can abandon that +stack, so the pointer would dangle. A later use of ZA would write through +it. + +`__arm_za_disable` is the helper `setjmp` uses. If the thread has SME, it +writes ZA into the buffer of the function that called `setjmp`, clears +`TPIDR2_EL0`, and turns ZA off. The compiler at that same call site turns +ZA back on and copies the buffer back. The address is a local in that +frame, so clearing `TPIDR2_EL0` does not lose it. `longjmp` returns to +that same frame, and the reload puts back the snapshot from `setjmp`. + +That is a single call site resuming itself. `stackman_switch` resumes +whichever stack the callback selects. The incoming activation's ZA is not +in the callee-saved spill, and its call site was not compiled to reload a +lazy-save buffer. Calling the helper on the way out would commit the +outgoing caller's ZA and turn ZA off, and nothing in the switch would +install the incoming caller's ZA. + +Leave the helper uncalled. A stack that uses SME would need its own ZA +image saved and restored with that stack, which this port does not do. +`stackman_call` returns to the same caller and does not need it either. + ## What the current tests cover `tests/test.c` already exercises a lot of the control path: @@ -208,11 +484,11 @@ Do not add a per-register assembly harness yet. A post-return sentinel check is the usual way to prove callee-saved registers survive. It is a poor fit here. -- A post-return check would have missed the old Windows `x18` bug. That - routine saved `x18` and wrote it back before `ret`. The damage was the - live TEB during the callback, so the harness would have seen the restored - value and passed. Sampling `x18` inside the callback only means something - on Windows, and the Windows ARM64 job does not run tests. +- It misses the Windows `x18` bug. That routine saves `x18` and writes it + back before `ret`. The damage is the live TEB during the callback. The + harness would see the restored value and pass. Sampling `x18` inside the + callback only means something on Windows, and the Windows ARM64 job does + not run tests. - It needs a different `.S` or `.asm` for each ABI, kept in lockstep with the spill list. The known mistakes above were found by reading those lists against the ABI. A harness that encodes the same list will not @@ -236,17 +512,24 @@ only for the no-op switch: 3. `stackman_switch` returns the same stack pointer. 4. The harness compares the sentinels, then restores the original registers. -That catches "forgot to spill `d8`" on AArch64. It would not have caught +That catches "forgot to spill `d8`" on AArch64. It still does not catch "`x18` was wrong for the duration of the call", and it does not belong in the first patch. +**Applied fix.** The `x18` bug itself was fixed, as described above. No +harness was added. A post-return sentinel would still miss a bug of that +shape. + ### Optional `fenv` check, still in C `fesetround` before `stackman_switch`, a callback that changes the rounding mode and returns, then read the mode again. Expect the original mode where -the assembly saves the control word (SysV x86-64 MXCSR and x87 control -word, SysV i386 x87 control word, Windows x64 MXCSR, Windows x86 x87 -control word). Expect the callback's mode to stick where the ABI treats -that register as thread state and the assembly does not save it (AAPCS64 -`FPCR`, RISC-V `fcsr`). This is ABI-specific and easy to get wrong; land -the `stackman_call` contract tests first. +the assembly saves the control word (SysV x86-64). Expect the callback's +mode to stick where the ABI treats that register as thread state and the +assembly does not save it (AAPCS64 `FPCR`, RISC-V `fcsr`). This is +ABI-specific and easy to get wrong; land the `stackman_call` contract tests +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). From 6cee165de075f612820e82910b652252775979ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 13:58:09 +0000 Subject: [PATCH 12/15] Replace the Copilot instructions with a root AGENTS.md. The Copilot file still described version 1.0.1, seven ABIs, and six tests, and told agents to trust it ahead of the tree. AGENTS.md points at the current sources instead. The Copilot path is a short pointer so that file does not drift again. --- .github/copilot-instructions.md | 141 +------------------------------- AGENTS.md | 141 ++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 138 deletions(-) create mode 100644 AGENTS.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5d36dae..d508ec1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,139 +1,4 @@ -# Copilot Instructions for stackman +# Copilot instructions -## Overview -Low-level C library for stack manipulation (continuations/co-routines). ~600 lines of C + assembly. Zero dependencies. **Current version: 1.0.1** - -**Supported Platforms (7 ABIs):** -- Linux: sysv_amd64, sysv_i386, arm32 (AAPCS), aarch64 (AAPCS64) -- Windows: win_x86, win_x64, win_arm64 - -**Note:** Windows ARM32 (win_arm) support was removed in v1.0.0 (Microsoft SDK 10.0.26100.0+ dropped support) - -**Toolchains:** GCC, Clang, MSVC (VS2017, VS2019, VS2022) - -## Build Commands (FAST: <1 second) - -**Always run in order:** -```bash -make clean # Clean build artifacts -make all # Build library (0.1s) → lib/[ABI]/libstackman.a -make test # Build + run 4 test suites (0.7s) -make abiname # Print platform ABI (e.g., sysv_amd64) -``` - -**Success output:** `*** All test suites passed ***` - -**Cross-compile x86:** `make PLATFORMFLAGS=-m32 test` -**Cross-compile ARM:** `make PLATFORM_PREFIX=arm-linux-gnueabi- EMULATOR=qemu-arm test` -**Windows:** `msbuild vs2022\stackman.sln /p:Platform=x64` then `vs2022\x64\Debug\test.exe` - -## Critical Build Notes - -1. **Intel CET:** `-fcf-protection=none` flag REQUIRED (auto-added by disable_cet script). Stack switching incompatible with Shadow Stack. -2. **Libraries Deprecated in Repo:** As of v1.0.1, pre-built libraries are NO LONGER automatically committed. Download from [GitHub Releases](https://github.com/stackless-dev/stackman/releases) instead. `lib/` directory will be removed in v2.0.0. -3. **Expected Warning:** Linker warning "missing .note.GNU-stack section" in test_asm is NORMAL - ignore it. -4. **Artifacts:** `*.o`, `bin/`, `tmp/` NOT committed. Libraries available via GitHub Releases. -5. **Incremental OK:** After code changes, just `make test`. Only clean when switching platforms. - -## Project Structure - -**Key Directories:** -- `stackman/` - Main source: `stackman.h` (API + version macros), `stackman_switch.h`, `stackman_impl.h`, `platforms/` (15+ platform files) -- `tests/` - 4 test files: `test.c` (6 tests), `test_cc.cc`, `test_static.c`, `test_asm.c/.S` -- `lib/[ABI]/` - Pre-built libraries (DEPRECATED - see lib/README.md, will be removed in v2.0.0) -- `vs2017/`, `vs2019/`, `vs2022/` - Visual Studio projects -- `tools/` - `abiname.sh`, `abiname.c`, `strip-lib.py` - -**Core API (2 functions only):** -```c -void *stackman_switch(stackman_cb_t callback, void *context); // Main stack switch -void *stackman_call(stackman_cb_t callback, void *context, void *stack); // Call with different stack -``` - -**Architecture:** `platforms/platform.h` detects OS/arch/compiler → includes appropriate `switch_[abi]_[compiler].h/S/asm` - -## CI Validation (.github/workflows/buildcommit.yml) - -**Triggers:** Push to master/dev, PRs to master, tags (v*.*.*) - -**Jobs:** -1. **build-linux-gnu** (sysv_amd64, sysv_i386, arm32, aarch64) - installs cross-tools → `make all` → `make test` (qemu for ARM) -2. **build-windows** (win_x86, win_x64, win_arm64) - MSBuild → strip-lib.py → rebuild (MUST rebuild after strip!) -3. **commit-artifacts** (DISABLED as of v1.0.1) - Previously committed libs to repo, now disabled (if: false) -4. **create-release** (tags only) - Creates GitHub Release with `stackman-{version}.tar.gz` containing all platforms - -**Release Archive Contents:** -- `stackman/` - Headers -- `lib/{abi}/` - Pre-built libraries for all 7 platforms -- `tools/` - Platform detection tools -- `Makefile` - For `make abiname` -- Documentation (README.md, LICENSE, CHANGELOG.md) - -**Local validation:** -```bash -make clean && make test # Test native platform -git status # Verify no bin/, tmp/, *.o tracked -``` - -## Key Patterns & Workarounds - -**Making Changes:** -- Platform code: edit `stackman/platforms/switch_*.h` or `.S` - reference `switch_template.h` -- Always run `make test` after changes (fast: 0.7s) -- Test on actual hardware if modifying assembly (arch-specific!) - -**Known Issues/Workarounds:** -- **CET:** `-fcf-protection=none` REQUIRED (auto-added by disable_cet script) -- **Inline asm:** May be inlined by optimizer → use separate .S files or volatile pointer (see stackman_impl.h) -- **Stack align:** Use `STACKMAN_SP_ALIGN` macro - -**Testing:** 4 test executables, 6 tests each (assertions fail hard). Success: "test_XX ok" + "*** All test suites passed ***" - -**Include patterns:** -- User code: `#include "stackman.h"` -- Library impl: `#include "stackman_impl.h"` (once) - -## Configuration Files -- **Build:** Makefile (Linux), vs2022/*.vcxproj (Windows) -- **CI:** .github/workflows/buildcommit.yml -- **Versioning:** stackman/stackman.h - STACKMAN_VERSION_MAJOR/MINOR/PATCH macros + STACKMAN_VERSION string -- **Changelog:** CHANGELOG.md (documents all releases) -- **Linting:** None configured -- **Testing:** `make test` target -- **.gitignore:** Excludes *.o, bin/, tmp/ BUT still includes lib/**/*.a, lib/**/*.lib (deprecated, slated for removal) - -## Development Tips - -1. **Trust these instructions first** - search only if info incomplete/incorrect -2. **Build is FAST** - rebuild freely (clean+test <1s) -3. **Test after every change** - `make test` is fast and comprehensive -4. **Cross-compilation optional** - CI validates all platforms, native x64 sufficient for most changes -5. **No binary commits** - lib/ directory deprecated, use GitHub Releases -6. **Zero dependencies** - don't add any -7. **Minimal changes** - stable library, surgical edits only -8. **Low-level code** - assembly is platform-specific, test on actual hardware - -## Versioning & Releases - -**Version Location:** `stackman/stackman.h` -```c -#define STACKMAN_VERSION_MAJOR 1 -#define STACKMAN_VERSION_MINOR 0 -#define STACKMAN_VERSION_PATCH 1 -#define STACKMAN_VERSION "1.0.1" -``` - -**Release Process:** -1. Update version in `stackman/stackman.h` (MAJOR.MINOR.PATCH) -2. Update `CHANGELOG.md` with changes -3. Commit changes: `git commit -m "Bump version to X.Y.Z"` -4. Create tag: `git tag -a vX.Y.Z -m "Release vX.Y.Z"` -5. Push: `git push upstream master && git push upstream vX.Y.Z` -6. CI automatically creates GitHub Release with pre-built libraries - -**Version History:** -- v0.1 (2020-05-18) - Initial release -- v0.2 (retroactive tag) - Pre-1.0 development state -- v1.0.0 (2025-11-16) - First stable release, removed Windows ARM32 -- v1.0.1 (2025-11-16) - Disabled library commits, use GitHub Releases -- v2.0.0 (future) - Will remove lib/ directory entirely +The project instructions are in [AGENTS.md](../AGENTS.md) at the +repository root. Read that file. Do not keep a second copy here. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c304131 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,141 @@ +# Agent notes for stackman + +Low-level C library for stack switching. Two functions, no dependencies. +Read the sources. This file is a map, not a second copy of them. The +version is `STACKMAN_VERSION` in `stackman/stackman.h`. + +## What the library does + +`stackman_switch` saves callee-saved state, calls the callback with +`STACKMAN_OP_SAVE`, installs the stack pointer that callback returns, +calls it again with `STACKMAN_OP_RESTORE`, then restores the saved state +and returns that second result. It may resume a different caller, so the +spill is the whole non-volatile set for that ABI. + +`stackman_call` runs the callback on another stack and returns to the +same caller. A null stack is left unchanged. The pointer passed to the +callback is the previous stack pointer. It only needs to preserve +registers it modifies. + +User code includes `stackman.h`. The library implementation includes +`stackman_impl.h` once. `platforms/platform.h` selects +`switch__`. + +`STACKMAN_HAVE_CALL` and `STACKMAN_STACK_ALIGN` must be visible from a +normal `#include "stackman.h"`. If they sit under +`#ifdef STACKMAN_SWITCH_IMPL`, `tests/test.c` compiles `stackman_call` +out and CI looks green. + +Align a stack with `STACKMAN_SP_ALIGN`. It follows `STACKMAN_STACK_ALIGN` +and the stack direction. + +## Platforms + +Linux: `sysv_i386`, `sysv_amd64`, `arm32` (soft-float AAPCS), `aarch64`, +`riscv64` (LP64D). macOS: `darwin_x86_64`, `darwin_arm64`. Windows: +`win_x86`, `win_x64`, `win_arm64`. `win_aarch64` is accepted and +normalized to `win_arm64`. FreeBSD, OpenBSD, and NetBSD use the Linux +`sysv_amd64` sources. Their workflow runs `gmake`. + +Windows ARM32 (`win_arm`) was removed in 1.0.0. Do not add it back. + +The default build uses the checked-in `.S` and `.asm` files. +`STACKMAN_INLINE_ASM` is unset. RISC-V always uses +`switch_riscv64_gcc.S`. When a `.S` is generated from a header, change +the header and the `.S` together. `gen_asm.c` defines +`STACKMAN_INLINE_ASM` as 1. A `.S` `#` line comment is a snapshot of the +header line and goes stale if lines are inserted above the asm. Leave +those comments unless you are regenerating the file. + +## Register contract + +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`. +Limits that are easy to fix the wrong way: + +- ARM32 hard-float is unsupported. `switch_arm_gcc.S` is `.arch armv5t` + and `.fpu softvfp`, and it does not spill `d8`–`d15`. `__ARM_PCS_VFP` + is a compile error. A hard-float port is a new assembly file with its + own ABI name. Softfp does not define `__ARM_PCS_VFP`, and the compiler + can still use `d8`–`d15`. +- `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 + switch installs the incoming stack's ZA. `stackman_call` returns to + the same caller and does not need it. Windows ARM64 is a different + runtime. Do not call the AAPCS64 helper there. +- On Windows ARM64, `x18` is the TEB for the whole time user code runs. + Spill and restore the saved value. Do not write the live register. + Do not invent `.seh_*` names for this file. +- A live frame pointer (`x29` on AArch64, `r11` on ARM32) is adjusted by + the stack delta inside the asm. Do not list that register as a + clobber. 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. + +## Build and test + +```bash +make test # native ABI. Builds lib/$(ABI)/libstackman.a and runs the tests. +make abiname +make PLATFORMFLAGS=-m32 test +make PLATFORM_PREFIX=arm-linux-gnueabi- EMULATOR=qemu-arm test +``` + +`make clean` only when switching `PLATFORMFLAGS` or `PLATFORM_PREFIX`. +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` +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. + +The linker warning `missing .note.GNU-stack section` on +`tests/test_asm_s.o` is expected. + +Intel CET: the Makefile adds `-fcf-protection=none` through +`./disable_cet`. Stack switching is incompatible with the shadow stack. +Regenerating asm uses the same flag (`gcc -S -fcf-protection=none`). + +Windows, as CI runs it: `msbuild vs2022\stackman.sln /p:Platform=x64` +(also `x86` and `arm64`). Native tests are `vs2022\x64\Debug\test.exe` +and `vs2022\Win32\Debug\test.exe`. The arm64 build is not run. After +`tools/strip-lib.py`, CI rebuilds. + +Do not commit rebuilt `lib/` archives, `*.o`, `bin/`, or `tmp/`. +Checked-in libraries under `lib/` are deprecated and go away in a future +major version. `commit-artifacts` in +`.github/workflows/buildcommit.yml` is `if: false`. + +## CI + +`.github/workflows/buildcommit.yml`, on pull requests to `master`: + +- `build-linux-gnu`: AMD64, i386 (`-m32`), arm (`qemu-arm`), aarch64 + (`qemu-aarch64`), riscv64 (`qemu-riscv64`). Each runs `make test`. +- `build-macos`: `macos-15-intel` (`darwin_x86_64`) and `macos-latest` + (`darwin_arm64`). +- `build-windows`: VS2022 `x86`, `x64`, and `arm64`. Tests run only + where `native` is set (`x86` and `x64`). + +`.github/workflows/buildbsd.yml` builds and tests FreeBSD, OpenBSD, and +NetBSD with `gmake` on the `sysv_amd64` sources. + +A green native `make test` says nothing about another ABI. An assembly +change is checked by the job that builds that file. + +## Edits + +Keep changes small. Do not add a dependency. Do not bump `stackman.h` or +`CHANGELOG.md` unless asked. + +Prefer the checked-in assembly over turning `STACKMAN_INLINE_ASM` on. +`switch_template.h` is the skeleton for a new port. + +There is no `main` branch. `master` is the default on both `origin` +(`kristjanvalur/stackman`) and `upstream` (`stackless-dev/stackman`). From bdef93af49ba7759cda5b4a4608f15f43a0e6735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 14:00:02 +0000 Subject: [PATCH 13/15] commit za comment --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 240b420..9202264 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,9 @@ calling convention, plus archive format: - **FreeBSD, OpenBSD, NetBSD (System V ABI)** - sysv_amd64 (64-bit x86_64) +The 64-bit ARM ABIs (`aarch64`, `darwin_arm64`, and `win_arm64`) do not +preserve SME ZA context across `stackman_switch`. + 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 From 76af44746a5b6965e5949a85ba0024baf033da4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 16:37:21 +0000 Subject: [PATCH 14/15] Drop the ARM32 r11 adjust from the stack switch. stackman_switch is built with omit-frame-pointer. The prologue pushes fp and never sets it, so live r11 is not this frame, and the epilogue pops the saved value. The inline build can keep the callback or the new stack pointer in r11, and the delta then corrupts it before the restore callback. AArch64 still adjusts x29, which the prologue sets from sp. --- AGENTS.md | 12 +++++++----- docs/abi-register-audit.md | 22 ++++++++++++++-------- stackman/platforms/switch_arm_gcc.S | 4 +--- stackman/platforms/switch_arm_gcc.h | 12 ++---------- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c304131..47b5ec1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,11 +69,13 @@ Limits that are easy to fix the wrong way: - On Windows ARM64, `x18` is the TEB for the whole time user code runs. Spill and restore the saved value. Do not write the live register. Do not invent `.seh_*` names for this file. -- A live frame pointer (`x29` on AArch64, `r11` on ARM32) is adjusted by - the stack delta inside the asm. Do not list that register as a - clobber. 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. +- A live frame pointer (`x29` on AArch64) is adjusted by the stack delta + inside the asm. Do not list `x29` as a clobber. Do not adjust ARM32 + `r11` there: `stackman_switch` is built with `omit-frame-pointer`, the + prologue pushes `r11` and never sets it, and the epilogue reloads the + 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. diff --git a/docs/abi-register-audit.md b/docs/abi-register-audit.md index 4460873..8268fad 100644 --- a/docs/abi-register-audit.md +++ b/docs/abi-register-audit.md @@ -179,9 +179,13 @@ file and does not run it. to the callback. The C wrapper still restores `fs:[0]`. - `switch_arm_gcc.h` is a compile error when `__ARM_PCS_VFP` is set. `d8`–`d15` were not added. `CP_REGISTERS` is marked as the inline-only - clobber. `stackman_switch` applies the stack delta to live `r11` - (`sub r1, sp, r0` / `mov sp, r0` / `sub r11, r11, r1`) in the `.S` and - in the header. The epilogue still pops the saved `r11`. + clobber. An `r11` stack delta was added and then removed. + `stackman_switch` is compiled with `omit-frame-pointer`, and the + prologue pushes `fp` without setting it, so live `r11` is not this + frame. The epilogue pops the saved value. An inline build can also + 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`. - 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 @@ -272,8 +276,10 @@ uses `s0` to hold the previous stack pointer across the callback, so do not adjust it there. **Applied fix.** The `x29` delta is in `switch_aarch64_gcc.S` and in -`switch_aarch64_gcc.h`. The `r11` delta is in `switch_arm_gcc.S` and in -`switch_arm_gcc.h`. The RISC-V `s0` delta was left unchanged. +`switch_aarch64_gcc.h`. The `r11` delta was added to `switch_arm_gcc.S` +and `switch_arm_gcc.h`, then removed: that function omits the frame +pointer, so the delta does not land on this frame. The RISC-V `s0` delta +was left unchanged. ### AArch64 SME lazy save @@ -348,9 +354,9 @@ The live `r11` adjustment in the AArch64 section above is optional here. 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 as -well. `d8`–`d15` were not added. A separate hard-float port is on the -roadmap. +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. A separate hard-float port is on the roadmap. ### Windows ARM32: delete it, or align `sp` before `vpush` diff --git a/stackman/platforms/switch_arm_gcc.S b/stackman/platforms/switch_arm_gcc.S index c8b15e6..84929f8 100644 --- a/stackman/platforms/switch_arm_gcc.S +++ b/stackman/platforms/switch_arm_gcc.S @@ -43,10 +43,8 @@ stackman_switch: blx r3 mov r2, r0 .syntax divided -@ 97 "../platforms/switch_arm_gcc.h" 1 - sub r1, sp, r0 +@ 93 "../platforms/switch_arm_gcc.h" 1 mov sp, r0 - sub r11, r11, r1 @ 0 "" 2 .arm .syntax unified diff --git a/stackman/platforms/switch_arm_gcc.h b/stackman/platforms/switch_arm_gcc.h index b0f2e99..2ad70a6 100644 --- a/stackman/platforms/switch_arm_gcc.h +++ b/stackman/platforms/switch_arm_gcc.h @@ -89,16 +89,8 @@ void *STACKMAN_SWITCH_INASM_NAME(stackman_cb_t callback, void *context) __asm__ ("mov %[result], sp" : [result] "=r" (sp)); /* store stack */ sp = callback(context, STACKMAN_OP_SAVE, sp); - /* Keep the caller's frame pointer attached to the new stack. - * r1 is free until the compiler assigns the RESTORE opcode. - * r11 is not an asm clobber: a clobber would make gcc save - * and restore it around this block and undo the delta. - */ - __asm__ ( - "sub r1, sp, %[result]\n\t" - "mov sp, %[result]\n\t" - "sub r11, r11, r1" - : : [result] "r" (sp) : "r1"); + /* set stack pointer from sp using assembly */ + __asm__ ("mov sp, %[result]" : : [result] "r" (sp)); sp = callback(context, STACKMAN_OP_RESTORE, sp); /* restore registers */ return sp; From e08f69779afc0a013eed8c10410c83be019aaa23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 14 Oct 2026 16:37:21 +0000 Subject: [PATCH 15/15] Correct the i386 fstcw comment. fstcw is FWAIT plus FNSTCW, so it does wait on a pending unmasked x87 exception. The spill stays fstcw, matching switch_x86_64_gcc.S, which delivers that exception on this stack before the switch. --- stackman/platforms/switch_x86_gcc.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stackman/platforms/switch_x86_gcc.h b/stackman/platforms/switch_x86_gcc.h index dc45133..d93ded1 100644 --- a/stackman/platforms/switch_x86_gcc.h +++ b/stackman/platforms/switch_x86_gcc.h @@ -23,8 +23,9 @@ * eax, ecx, edx are scratch regs, * ebp, ebx, esi, edi and the x87 control word are callee-preserved. * The control word is stored at the stack pointer passed to the callback - * so a copied stack image carries it. fstcw does not wait on pending - * exceptions, matching switch_x86_64_gcc.S. + * so a copied stack image carries it. fstcw matches switch_x86_64_gcc.S. + * It is FWAIT plus FNSTCW, so a pending unmasked exception is delivered + * on this stack before the switch. * We have compiler construct a frame and push ebp * an then we fix an aligned stack pointer and just store * the arguments in proper places.