Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/buildcommit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ jobs:
- name: make
run: make all
- name: test
timeout-minutes: 1
run: make test
- name: set abi name
run: echo abiname=$(make abiname) >> $GITHUB_ENV
Expand Down Expand Up @@ -126,6 +127,7 @@ jobs:
run: make all

- name: Run tests
timeout-minutes: 1
run: make test

- name: Upload artifacts
Expand Down Expand Up @@ -160,6 +162,7 @@ jobs:
run: msbuild.exe vs2022\stackman.sln /p:Platform=${{matrix.platform}}
- name: test
if: ${{ matrix.native == 'yes' }}
timeout-minutes: 1
run: vs2022\${{matrix.folder}}\Debug\test.exe
- name: Upload build artifacts
uses: actions/upload-artifact@v7.0.0
Expand Down
28 changes: 21 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,19 @@ those comments unless you are regenerating the file.
Volatile registers may change. Do not add a test that treats them as
preserved.

Details and the original audit are in `docs/abi-register-audit.md`.
Current spills are in `docs/platform-abi.md`.
`docs/abi-register-audit.md` is the audit from when those gaps were
found and fixed. Do not edit the audit for a later port or spill.
Limits that are easy to fix the wrong way:

- `arm32` is the soft-float assembly (`switch_arm_gcc.S`, `.arch armv5t`,
`.fpu softvfp`). It does not spill `d8`–`d15`, and `__ARM_PCS_VFP` is
a compile error in that header. `armhf` (`switch_armhf_gcc.S`) is the
hard-float port and is what `__ARM_PCS_VFP` selects. It spills
`d8`–`d15` in `stackman_switch`. It does not spill `FPSCR`. Softfp
does not define `__ARM_PCS_VFP` and still builds as `arm32`.
`d8`–`d15` and the whole `FPSCR` in `stackman_switch`. Softfp
(`-mfloat-abi=softfp`) is not a supported platform. It still builds
as `arm32` and runs, and that assembly does not preserve `FPSCR`.
Do not add an `FPSCR` spill to `switch_arm_gcc.S` to cover it.
- `aarch64`, `darwin_arm64`, and `win_arm64` do not preserve SME ZA
across `stackman_switch`. Do not call `__arm_za_disable`. That helper
commits the outgoing caller's ZA and turns ZA off. Nothing in the
Expand All @@ -78,8 +82,10 @@ Limits that are easy to fix the wrong way:
saved value. On RISC-V, `s0` is not a frame chain in
`stackman_switch`, and `stackman_call` uses `s0` for the previous
stack pointer. Do not adjust it there.
- AAPCS64 FPCR and RISC-V `fcsr` are thread state. They are not per-call
spills.
- `stackman_switch` spills `FPCR` and `FPSR` on `aarch64`,
`darwin_arm64`, and `win_arm64`, and spills `fcsr` on `riscv64`.
The RISC-V slot is the existing 8-byte pad at the published stack
pointer. `stackman_call` does not touch these registers.

## Build and test

Expand All @@ -95,10 +101,14 @@ make PLATFORM_PREFIX=arm-linux-gnueabihf EMULATOR=qemu-arm test
Success ends with `*** All test suites passed ***`.

Four programs: `bin/test`, `bin/test_cc`, `bin/test_static`,
`bin/test_asm`. `tests/test.c` is `test_01` through `test_08`. `test_04`
`bin/test_asm`. `tests/test.c` is `test_01` through `test_09`. `test_04`
and `test_05` are the `stackman_call` contract. `test_07` and `test_08`
pressure the compiler. They pass even when a callee-saved register is
dropped, if the compiler kept the values in memory.
dropped, if the compiler kept the values in memory. `test_09` checks
that `stackman_switch` restores the caller's rounding mode, including
across a stack switch. Soft-float `arm32` does not, and that test
expects the callback's mode to stick. The test programs link `-lm`
for `fesetround`.

The linker warning `missing .note.GNU-stack section` on
`tests/test_asm_s.o` is expected.
Expand Down Expand Up @@ -132,6 +142,10 @@ major version. `commit-artifacts` in
`.github/workflows/buildbsd.yml` builds and tests FreeBSD, OpenBSD, and
NetBSD with `gmake` on the `sysv_amd64` sources.

The test steps use `timeout-minutes: 1`. A passing run finishes in a few
seconds. On Windows an `assert` opens a CRT dialog and does not exit, so
that limit is what ends a failed `test.exe`.

A green native `make test` says nothing about another ABI. An assembly
change is checked by the job that builds that file.

Expand Down
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ tests: bin/test_asm
tests: LDLIBS := -lstackman

bin/test: tests/test.o $(LIB)/libstackman.a
$(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $< ${DEBUG} $(LDLIBS)
$(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $< ${DEBUG} $(LDLIBS) -lm

bin/test_cc: tests/test_cc.o $(LIB)/libstackman.a
$(CXX) $(LDFLAGS) $(STATIC_FLAG) -o $@ $< ${DEBUG} $(LDLIBS)
$(CXX) $(LDFLAGS) $(STATIC_FLAG) -o $@ $< ${DEBUG} $(LDLIBS) -lm

bin/test_static: tests/test_static.o
$(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $^ ${DEBUG}
$(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $^ ${DEBUG} -lm

bin/test_asm: tests/test_asm.o tests/test_asm_s.o
$(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $^ ${DEBUG}
$(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $^ ${DEBUG} -lm
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,17 @@ calling convention, plus archive format:
- **FreeBSD, OpenBSD, NetBSD (System V ABI)**
- sysv_amd64 (64-bit x86_64)

Which registers each ABI spills is listed in
[`docs/platform-abi.md`](docs/platform-abi.md).

The 64-bit ARM ABIs (`aarch64`, `darwin_arm64`, and `win_arm64`) do not
preserve SME ZA context across `stackman_switch`.

Softfp (`-mfloat-abi=softfp`) is not a supported platform, and it is not
clear how widely it is used. In practice that build does not define
`__ARM_PCS_VFP`, so it selects `arm32` and runs. The soft-float assembly
does not preserve `FPSCR`.

All platforms are automatically built and tested by GitHub Actions CI on every commit.

The BSDs share the `sysv_amd64` ABI with Linux and therefore need no separate
Expand Down
35 changes: 19 additions & 16 deletions docs/abi-register-audit.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# ABI register audit

This is the historical record of the register audit in
[pull request 21](https://github.com/stackless-dev/stackman/pull/21).
The text below is that pull request's outcome, including gaps a later
change has closed. It is not a description of the current tree.
Later ports and spills belong in
[`platform-abi.md`](platform-abi.md), not here.

Audit of the assembly that implements `stackman_switch` and `stackman_call`.
The question is whether each routine spills every non-volatile register to the
stack before calling out, and loads that image back before returning.
Expand Down Expand Up @@ -158,7 +165,7 @@ file and does not run it.
| `switch_x64_msvc.asm` | `rbx`, `rbp`, `rdi`, `rsi`, `r12`–`r15`, `xmm6`–`xmm15` | Those registers match the Windows x64 ABI. MXCSR[6:15] is non-volatile on Windows and is not spilled, so a changed rounding mode does not travel with the stack. The SysV file does save MXCSR. |
| `switch_x86_gcc.S` | `ebx`, `esi`, `edi`, `ebp` | Integer set is complete. There is no callee-saved XMM on i386 SysV. The x87 control word is callee-saved in that ABI and is not saved. |
| `switch_x86_msvc.asm` | `ebx`, `esi`, `edi`, `ebp`, plus the C wrapper saves `fs:[0]` | Integer set is complete for cdecl. Same x87 control-word gap. |
| `switch_arm_gcc.S` | `r4`–`r11`, `lr` | Complete for the soft-float `arm-linux-gnueabi` ABI this tree ships (`.arch armv5t`, `.fpu softvfp`). `d8`–`d15` are absent. The inline header clobbers them, so a hard-float or softfp build of this `.S` would drop callee-saved VFP state. The live frame pointer is not adjusted. |
| `switch_arm_gcc.S` | `r4`–`r11`, `lr` | Complete for the soft-float `arm-linux-gnueabi` ABI this tree ships (`.arch armv5t`, `.fpu softvfp`). `d8`–`d15` are absent. The inline header clobbers them, so a hard-float or softfp build of this `.S` would drop callee-saved VFP state. `armhf` is not a configured ABI. The live frame pointer is not adjusted. |
| `switch_arm_msvc.asm` | `r4`–`r11`, `lr`, `q4`–`q7` (`d8`–`d15`), `FPSCR` | The set matches 32-bit Windows ARM. Alignment does not. `push {r4-r10, fp, lr}` is 9 words, so `sp` is 4-byte aligned for `vpush {q4-q7}`. A doubleword `VPUSH` requires 8-byte alignment. `stackman_call` pushes 3 registers and then `blx`s with `sp` still 4 mod 8. The changelog records this ABI as dropped; the file is still wrong if it is built. |
| `switch_riscv64_gcc.S` | `ra`, `s0`–`s11`, `fs0`–`fs11` | `stackman_switch` is complete for LP64D. `gp` and `tp` are left alone. `fcsr` is thread `fenv` state, not a per-call spill. |

Expand Down Expand Up @@ -186,9 +193,6 @@ file and does not run it.
keep the callback or the new stack pointer in `r11` across that asm.
AArch64 still adjusts `x29`, which the prologue sets with
`mov x29, sp`.
- `__ARM_PCS_VFP` selects `armhf` (`switch_armhf_gcc.S`), which spills
`d8`–`d15`. `FPSCR` is still not spilled. The soft-float file was
left unchanged.
- Windows ARM32 was removed: `switch_arm_msvc.asm`, `switch_arm_msvc.h`,
the `_M_ARM` branch in `platform.h`, the Debug|ARM configurations in
vs2017, vs2019, and vs2022, and `lib/win_arm/stackman.lib`. The
Expand Down Expand Up @@ -359,8 +363,7 @@ The shipped integer set (`r4`–`r11`, `lr`) needs no new stores.
**Applied fix.** `__ARM_PCS_VFP` is a compile error, and `CP_REGISTERS` is
commented as inline-only. The optional `r11` adjustment was applied and
then removed. See the frame-pointer applied fix above. `d8`–`d15` were
not added to this file. `__ARM_PCS_VFP` selects `armhf`
(`switch_armhf_gcc.S` spills `d8`–`d15`; `FPSCR` is still not spilled).
not added. A separate hard-float port is on the roadmap.

### Windows ARM32: delete it, or align `sp` before `vpush`

Expand Down Expand Up @@ -397,20 +400,20 @@ alternative was not used.

## Roadmap

SME lazy save is still open. The ARM32 hard-float port is `armhf`.
Two items from the audit are still open.

### ARM32 hard-float

The soft-float file `switch_arm_gcc.S` is unchanged (`arm-linux-gnueabi`,
The shipped `switch_arm_gcc.S` is soft-float (`arm-linux-gnueabi`,
`.arch armv5t`, `.fpu softvfp`) and does not spill `d8`–`d15`.
`__ARM_PCS_VFP` is still a compile error in that header, so a hard-float
build cannot use that file by mistake.

`switch_armhf_gcc.h` is the hard-float port. `platform.h` selects it,
and the ABI name `armhf`, when `__ARM_PCS_VFP` is set. `stackman_switch`
spills `d8`–`d15`. `FPSCR` is not spilled. Softfp is not this port: it
does not define `__ARM_PCS_VFP`, and the compiler can still use
`d8`–`d15` while building `arm32`.
`__ARM_PCS_VFP` is a compile error, so a hard-float build cannot use that
file by mistake.

A hard-float port is a new assembly, alongside the soft-float file. It
spills `d8`–`d15` in `stackman_switch`, has its own ABI name, and is what
a `__ARM_PCS_VFP` build selects. The `#error` stays until that port
exists. Softfp is not that port: it does not define `__ARM_PCS_VFP`, and

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This edit puts the historical audit back into the present tense and denies the armhf port that this branch builds on. It says __ARM_PCS_VFP keeps erroring until a hard-float port exists, and the switch_arm_gcc.S row now says armhf is not a configured ABI. platform.h already selects switch_armhf_gcc.h and the armhf ABI when __ARM_PCS_VFP is set, and this PR spills FPSCR in that port. The applied-fix note that a hard-float port is still on the roadmap has the same problem. The banner says later ports do not belong in this file; rewriting it so the current tree looks unfinished is worse than leaving the post-armhf text.

Suggestion: Leave the audit as the PR 21 record. Point current armhf and FPSCR behavior only at docs/platform-abi.md, and drop the new claim that armhf is not configured.

the compiler can still use `d8`–`d15`.

### AArch64 SME lazy save

Expand Down
166 changes: 166 additions & 0 deletions docs/platform-abi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Platform ABI and register spills

Current contract for `stackman_switch` and `stackman_call`. The assembly
and inline headers are the source of truth. This file is the map.

`docs/abi-register-audit.md` is the audit from when the gaps were found
and fixed. Do not add later ports or spills to that file.

## Switching

`stackman_switch` may resume a different caller. It spills the
non-volatile set for that ABI, calls the callback with
`STACKMAN_OP_SAVE`, installs the stack pointer that callback returns,
calls it again with `STACKMAN_OP_RESTORE`, restores the saved state,
and returns the second result.

`stackman_call` returns to the same caller. A null stack is left
unchanged. The pointer passed to the callback is the previous stack
pointer. The routine preserves only registers it modifies.

Volatile registers may change across either call.

A floating-point control or status register is spilled in
`stackman_switch` when a thread switch saves it. The whole register is
stored, including caller-saved flag bits. The store is in the frame
before that stack pointer is published to the callback. The reload is
after `STACKMAN_OP_RESTORE` and before the epilogue pops callee-saved
registers. `stackman_call` does not touch these registers.
`tests/test.c` `test_09` checks the caller's rounding mode. It comes
back on every ABI below except soft-float `arm32`, where the test
expects the callback's mode to remain.

`STACKMAN_SP_ALIGN` follows `STACKMAN_STACK_ALIGN` below.

## Platforms

`platform.h` selects the source. `win_aarch64` is accepted and
normalized to `win_arm64`. FreeBSD, OpenBSD, and NetBSD use the
`sysv_amd64` sources. Windows ARM32 (`win_arm`) was removed in 1.0.0.

| ABI | Source | `STACKMAN_STACK_ALIGN` |
| --- | --- | --- |
| `sysv_i386` | `switch_x86_gcc` | 16 |
| `sysv_amd64`, `darwin_x86_64` | `switch_x86_64_gcc` | 16 |
| `arm32` | `switch_arm_gcc` | 8 |
| `armhf` | `switch_armhf_gcc` | 8 |
| `aarch64`, `darwin_arm64` | `switch_aarch64_gcc` | 16 |
| `riscv64` | `switch_riscv64_gcc` | 16 |
| `win_x86` | `switch_x86_msvc` | 4 |
| `win_x64` | `switch_x64_msvc` | 16 |
| `win_arm64` | `switch_arm64_msvc` | 16 |

### `sysv_i386`

`stackman_switch` spills `ebx`, `esi`, `edi`, `ebp`, and the x87
control word (`fstcw` / `fldcw`). It does not spill MXCSR. There is no
callee-saved XMM register in this ABI.

`stackman_call` does not spill the x87 control word.

### `sysv_amd64` and `darwin_x86_64`

`stackman_switch` spills `rbx`, `rbp`, `r12`–`r15`, the x87 control
word (`fstcw` / `fldcw`), and MXCSR (`stmxcsr` / `ldmxcsr`). XMM
registers are volatile. `rbp` is adjusted by the stack delta when the
callback installs a new stack.

`stackman_call` does not spill the x87 control word or MXCSR.

### `arm32`

Soft-float AAPCS, `arm-linux-gnueabi`. The assembly is `.arch armv5t`
and `.fpu softvfp`. `__ARM_PCS_VFP` is a compile error in this header.

`stackman_switch` spills `r4`–`r11` and `lr`. It does not spill
`d8`–`d15` or `FPSCR`. `r11` is not a frame chain here:
`omit-frame-pointer` is set, the prologue pushes `r11` without writing
it, and the epilogue pops the saved value. Do not adjust `r11`.

`stackman_call` does not spill `d8`–`d15` or `FPSCR`.

Softfp (`-mfloat-abi=softfp`) is not a supported platform. It does not
define `__ARM_PCS_VFP`, so it selects `arm32` and runs, and this
assembly does not preserve `FPSCR`. Do not add `vmrs fpscr` to
`switch_arm_gcc.S`. That instruction is undefined without VFP. The
compiler can still use `d8`–`d15` while building `arm32`.

### `armhf`

Hard-float AAPCS-VFP, `arm-linux-gnueabihf`. `__ARM_PCS_VFP` selects
this ABI. It does not link with `arm32`.

`stackman_switch` spills `r4`–`r11`, `lr`, `d8`–`d15`, and the whole
`FPSCR`. The published stack pointer is the 8-byte `FPSCR` slot, below
the `d8`–`d15` spill. That slot is written back before those registers
are popped. `r3` in the integer push is padding. `r11` is not adjusted,
for the same reason as `arm32`.

`stackman_call` does not spill `d8`–`d15` or `FPSCR`.

### `aarch64` and `darwin_arm64`

`stackman_switch` spills `x19`–`x28`, `x29`, `x30`, and the low 64 bits
of `v8`–`v15` (`d8`–`d15`). The high 64 bits of `v8`–`v15` are
caller-saved and are not spilled. `x18` is left alone.

`FPCR` and `FPSR` are stored in a 16-byte slot at the pointer passed to
the callback and written back after `STACKMAN_OP_RESTORE`. `x29` is
adjusted by the stack delta. The prologue sets `x29` from `sp`, so the
delta is computed from the lowered stack pointer. Do not list `x29` as
an asm clobber.

`stackman_call` does not spill `FPCR`, `FPSR`, or `d8`–`d15`.

SME ZA is not preserved. Do not call `__arm_za_disable`.

### `win_arm64`

`stackman_switch` spills `x18`–`x28`, `x29`, `x30`, `d8`–`d15`, `FPCR`,
and `FPSR`. The frame is 192 bytes. `FPCR` is stored with `x18` at
`[sp, #16]`. `FPSR` is the 8-byte slot at `[sp, #176]`, next to `d15`,
with 8 bytes of padding at `[sp, #184]`. The body does not use `x18`
as a scratch. The epilogue reloads the spilled value. `x18` is the TEB
for the whole time user code runs. The frame pointer is adjusted by
the stack delta. No `.seh_*` unwind codes are emitted. The high 64
bits of `v8`–`v15` are not spilled.

`stackman_call` uses a 32-byte frame. It does not touch `x18`, `FPCR`,
or `FPSR`.

SME ZA is not preserved. Do not call the AAPCS64 `__arm_za_disable`
helper on Windows.

### `riscv64`

LP64D. `stackman_switch` spills `ra`, `s0`–`s11`, `fs0`–`fs11`, and the
whole `fcsr` (`frm` and the accrued exception flags). The frame is 208
bytes. `fcsr` uses the existing 8-byte pad at the published stack
pointer, below `fs11`. `gp` and `tp` are left alone. `s0` is not a frame
chain in `stackman_switch`. Do not adjust it.

`stackman_call` spills `ra` and `s0`. `s0` holds the previous stack
pointer. A null stack is left unchanged. It does not spill `fcsr`.

Vector CSRs (`vcsr`, `vl`, `vtype`, `vxrm`, `vxsat`) are not spilled.

### `win_x86`

`stackman_switch_raw` spills `ebx`, `esi`, `edi`, `ebp`, the x87
control word (`fstcw` / `fldcw`), and MXCSR (`stmxcsr` / `ldmxcsr`).
The published pointer is that 8-byte slot: the control word at `[esp]`,
MXCSR at `[esp+4]`. Both are spilled so older compilers, which emit
x87, and current MSVC, which emits SSE2, each keep their rounding mode.
The C wrapper around the raw routine saves and restores `fs:[0]`, the
SEH chain.

`stackman_call` does not spill the x87 control word or MXCSR. The SEH
save is only on `stackman_switch`.

### `win_x64`

`stackman_switch` spills `rbx`, `rbp`, `rdi`, `rsi`, `r12`–`r15`,
`xmm6`–`xmm15`, and MXCSR. MXCSR sits in the 8-byte pad at the end of
the 168-byte XMM area.

`stackman_call` does not spill MXCSR or `xmm6`–`xmm15`.
Loading
Loading