Skip to content

Commit bae6b74

Browse files
Test stackman_call's return value and previous stack pointer. (#21)
* 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. * update docs * fix riscv64 headers * 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. * 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. * 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. * 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. * 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. * 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. * 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. * update ZA information * 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. * commit za comment * 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. * 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.
1 parent 0941fb9 commit bae6b74

34 files changed

Lines changed: 822 additions & 566 deletions

‎.github/copilot-instructions.md‎

Lines changed: 3 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,4 @@
1-
# Copilot Instructions for stackman
1+
# Copilot instructions
22

3-
## Overview
4-
Low-level C library for stack manipulation (continuations/co-routines). ~600 lines of C + assembly. Zero dependencies. **Current version: 1.0.1**
5-
6-
**Supported Platforms (7 ABIs):**
7-
- Linux: sysv_amd64, sysv_i386, arm32 (AAPCS), aarch64 (AAPCS64)
8-
- Windows: win_x86, win_x64, win_arm64
9-
10-
**Note:** Windows ARM32 (win_arm) support was removed in v1.0.0 (Microsoft SDK 10.0.26100.0+ dropped support)
11-
12-
**Toolchains:** GCC, Clang, MSVC (VS2017, VS2019, VS2022)
13-
14-
## Build Commands (FAST: <1 second)
15-
16-
**Always run in order:**
17-
```bash
18-
make clean # Clean build artifacts
19-
make all # Build library (0.1s) → lib/[ABI]/libstackman.a
20-
make test # Build + run 4 test suites (0.7s)
21-
make abiname # Print platform ABI (e.g., sysv_amd64)
22-
```
23-
24-
**Success output:** `*** All test suites passed ***`
25-
26-
**Cross-compile x86:** `make PLATFORMFLAGS=-m32 test`
27-
**Cross-compile ARM:** `make PLATFORM_PREFIX=arm-linux-gnueabi- EMULATOR=qemu-arm test`
28-
**Windows:** `msbuild vs2022\stackman.sln /p:Platform=x64` then `vs2022\x64\Debug\test.exe`
29-
30-
## Critical Build Notes
31-
32-
1. **Intel CET:** `-fcf-protection=none` flag REQUIRED (auto-added by disable_cet script). Stack switching incompatible with Shadow Stack.
33-
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.
34-
3. **Expected Warning:** Linker warning "missing .note.GNU-stack section" in test_asm is NORMAL - ignore it.
35-
4. **Artifacts:** `*.o`, `bin/`, `tmp/` NOT committed. Libraries available via GitHub Releases.
36-
5. **Incremental OK:** After code changes, just `make test`. Only clean when switching platforms.
37-
38-
## Project Structure
39-
40-
**Key Directories:**
41-
- `stackman/` - Main source: `stackman.h` (API + version macros), `stackman_switch.h`, `stackman_impl.h`, `platforms/` (15+ platform files)
42-
- `tests/` - 4 test files: `test.c` (6 tests), `test_cc.cc`, `test_static.c`, `test_asm.c/.S`
43-
- `lib/[ABI]/` - Pre-built libraries (DEPRECATED - see lib/README.md, will be removed in v2.0.0)
44-
- `vs2017/`, `vs2019/`, `vs2022/` - Visual Studio projects
45-
- `tools/` - `abiname.sh`, `abiname.c`, `strip-lib.py`
46-
47-
**Core API (2 functions only):**
48-
```c
49-
void *stackman_switch(stackman_cb_t callback, void *context); // Main stack switch
50-
void *stackman_call(stackman_cb_t callback, void *context, void *stack); // Call with different stack
51-
```
52-
53-
**Architecture:** `platforms/platform.h` detects OS/arch/compiler → includes appropriate `switch_[abi]_[compiler].h/S/asm`
54-
55-
## CI Validation (.github/workflows/buildcommit.yml)
56-
57-
**Triggers:** Push to master/dev, PRs to master, tags (v*.*.*)
58-
59-
**Jobs:**
60-
1. **build-linux-gnu** (sysv_amd64, sysv_i386, arm32, aarch64) - installs cross-tools → `make all` → `make test` (qemu for ARM)
61-
2. **build-windows** (win_x86, win_x64, win_arm64) - MSBuild → strip-lib.py → rebuild (MUST rebuild after strip!)
62-
3. **commit-artifacts** (DISABLED as of v1.0.1) - Previously committed libs to repo, now disabled (if: false)
63-
4. **create-release** (tags only) - Creates GitHub Release with `stackman-{version}.tar.gz` containing all platforms
64-
65-
**Release Archive Contents:**
66-
- `stackman/` - Headers
67-
- `lib/{abi}/` - Pre-built libraries for all 7 platforms
68-
- `tools/` - Platform detection tools
69-
- `Makefile` - For `make abiname`
70-
- Documentation (README.md, LICENSE, CHANGELOG.md)
71-
72-
**Local validation:**
73-
```bash
74-
make clean && make test # Test native platform
75-
git status # Verify no bin/, tmp/, *.o tracked
76-
```
77-
78-
## Key Patterns & Workarounds
79-
80-
**Making Changes:**
81-
- Platform code: edit `stackman/platforms/switch_*.h` or `.S` - reference `switch_template.h`
82-
- Always run `make test` after changes (fast: 0.7s)
83-
- Test on actual hardware if modifying assembly (arch-specific!)
84-
85-
**Known Issues/Workarounds:**
86-
- **CET:** `-fcf-protection=none` REQUIRED (auto-added by disable_cet script)
87-
- **Inline asm:** May be inlined by optimizer → use separate .S files or volatile pointer (see stackman_impl.h)
88-
- **Stack align:** Use `STACKMAN_SP_ALIGN` macro
89-
90-
**Testing:** 4 test executables, 6 tests each (assertions fail hard). Success: "test_XX ok" + "*** All test suites passed ***"
91-
92-
**Include patterns:**
93-
- User code: `#include "stackman.h"`
94-
- Library impl: `#include "stackman_impl.h"` (once)
95-
96-
## Configuration Files
97-
- **Build:** Makefile (Linux), vs2022/*.vcxproj (Windows)
98-
- **CI:** .github/workflows/buildcommit.yml
99-
- **Versioning:** stackman/stackman.h - STACKMAN_VERSION_MAJOR/MINOR/PATCH macros + STACKMAN_VERSION string
100-
- **Changelog:** CHANGELOG.md (documents all releases)
101-
- **Linting:** None configured
102-
- **Testing:** `make test` target
103-
- **.gitignore:** Excludes *.o, bin/, tmp/ BUT still includes lib/**/*.a, lib/**/*.lib (deprecated, slated for removal)
104-
105-
## Development Tips
106-
107-
1. **Trust these instructions first** - search only if info incomplete/incorrect
108-
2. **Build is FAST** - rebuild freely (clean+test <1s)
109-
3. **Test after every change** - `make test` is fast and comprehensive
110-
4. **Cross-compilation optional** - CI validates all platforms, native x64 sufficient for most changes
111-
5. **No binary commits** - lib/ directory deprecated, use GitHub Releases
112-
6. **Zero dependencies** - don't add any
113-
7. **Minimal changes** - stable library, surgical edits only
114-
8. **Low-level code** - assembly is platform-specific, test on actual hardware
115-
116-
## Versioning & Releases
117-
118-
**Version Location:** `stackman/stackman.h`
119-
```c
120-
#define STACKMAN_VERSION_MAJOR 1
121-
#define STACKMAN_VERSION_MINOR 0
122-
#define STACKMAN_VERSION_PATCH 1
123-
#define STACKMAN_VERSION "1.0.1"
124-
```
125-
126-
**Release Process:**
127-
1. Update version in `stackman/stackman.h` (MAJOR.MINOR.PATCH)
128-
2. Update `CHANGELOG.md` with changes
129-
3. Commit changes: `git commit -m "Bump version to X.Y.Z"`
130-
4. Create tag: `git tag -a vX.Y.Z -m "Release vX.Y.Z"`
131-
5. Push: `git push upstream master && git push upstream vX.Y.Z`
132-
6. CI automatically creates GitHub Release with pre-built libraries
133-
134-
**Version History:**
135-
- v0.1 (2020-05-18) - Initial release
136-
- v0.2 (retroactive tag) - Pre-1.0 development state
137-
- v1.0.0 (2025-11-16) - First stable release, removed Windows ARM32
138-
- v1.0.1 (2025-11-16) - Disabled library commits, use GitHub Releases
139-
- v2.0.0 (future) - Will remove lib/ directory entirely
3+
The project instructions are in [AGENTS.md](../AGENTS.md) at the
4+
repository root. Read that file. Do not keep a second copy here.

‎AGENTS.md‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Agent notes for stackman
2+
3+
Low-level C library for stack switching. Two functions, no dependencies.
4+
Read the sources. This file is a map, not a second copy of them. The
5+
version is `STACKMAN_VERSION` in `stackman/stackman.h`.
6+
7+
## What the library does
8+
9+
`stackman_switch` saves callee-saved state, calls the callback with
10+
`STACKMAN_OP_SAVE`, installs the stack pointer that callback returns,
11+
calls it again with `STACKMAN_OP_RESTORE`, then restores the saved state
12+
and returns that second result. It may resume a different caller, so the
13+
spill is the whole non-volatile set for that ABI.
14+
15+
`stackman_call` runs the callback on another stack and returns to the
16+
same caller. A null stack is left unchanged. The pointer passed to the
17+
callback is the previous stack pointer. It only needs to preserve
18+
registers it modifies.
19+
20+
User code includes `stackman.h`. The library implementation includes
21+
`stackman_impl.h` once. `platforms/platform.h` selects
22+
`switch_<abi>_<compiler>`.
23+
24+
`STACKMAN_HAVE_CALL` and `STACKMAN_STACK_ALIGN` must be visible from a
25+
normal `#include "stackman.h"`. If they sit under
26+
`#ifdef STACKMAN_SWITCH_IMPL`, `tests/test.c` compiles `stackman_call`
27+
out and CI looks green.
28+
29+
Align a stack with `STACKMAN_SP_ALIGN`. It follows `STACKMAN_STACK_ALIGN`
30+
and the stack direction.
31+
32+
## Platforms
33+
34+
Linux: `sysv_i386`, `sysv_amd64`, `arm32` (soft-float AAPCS), `aarch64`,
35+
`riscv64` (LP64D). macOS: `darwin_x86_64`, `darwin_arm64`. Windows:
36+
`win_x86`, `win_x64`, `win_arm64`. `win_aarch64` is accepted and
37+
normalized to `win_arm64`. FreeBSD, OpenBSD, and NetBSD use the Linux
38+
`sysv_amd64` sources. Their workflow runs `gmake`.
39+
40+
Windows ARM32 (`win_arm`) was removed in 1.0.0. Do not add it back.
41+
42+
The default build uses the checked-in `.S` and `.asm` files.
43+
`STACKMAN_INLINE_ASM` is unset. RISC-V always uses
44+
`switch_riscv64_gcc.S`. When a `.S` is generated from a header, change
45+
the header and the `.S` together. `gen_asm.c` defines
46+
`STACKMAN_INLINE_ASM` as 1. A `.S` `#` line comment is a snapshot of the
47+
header line and goes stale if lines are inserted above the asm. Leave
48+
those comments unless you are regenerating the file.
49+
50+
## Register contract
51+
52+
Volatile registers may change. Do not add a test that treats them as
53+
preserved.
54+
55+
Details and the original audit are in `docs/abi-register-audit.md`.
56+
Limits that are easy to fix the wrong way:
57+
58+
- ARM32 hard-float is unsupported. `switch_arm_gcc.S` is `.arch armv5t`
59+
and `.fpu softvfp`, and it does not spill `d8`–`d15`. `__ARM_PCS_VFP`
60+
is a compile error. A hard-float port is a new assembly file with its
61+
own ABI name. Softfp does not define `__ARM_PCS_VFP`, and the compiler
62+
can still use `d8`–`d15`.
63+
- `aarch64`, `darwin_arm64`, and `win_arm64` do not preserve SME ZA
64+
across `stackman_switch`. Do not call `__arm_za_disable`. That helper
65+
commits the outgoing caller's ZA and turns ZA off. Nothing in the
66+
switch installs the incoming stack's ZA. `stackman_call` returns to
67+
the same caller and does not need it. Windows ARM64 is a different
68+
runtime. Do not call the AAPCS64 helper there.
69+
- On Windows ARM64, `x18` is the TEB for the whole time user code runs.
70+
Spill and restore the saved value. Do not write the live register.
71+
Do not invent `.seh_*` names for this file.
72+
- A live frame pointer (`x29` on AArch64) is adjusted by the stack delta
73+
inside the asm. Do not list `x29` as a clobber. Do not adjust ARM32
74+
`r11` there: `stackman_switch` is built with `omit-frame-pointer`, the
75+
prologue pushes `r11` and never sets it, and the epilogue reloads the
76+
saved value. On RISC-V, `s0` is not a frame chain in
77+
`stackman_switch`, and `stackman_call` uses `s0` for the previous
78+
stack pointer. Do not adjust it there.
79+
- AAPCS64 FPCR and RISC-V `fcsr` are thread state. They are not per-call
80+
spills.
81+
82+
## Build and test
83+
84+
```bash
85+
make test # native ABI. Builds lib/$(ABI)/libstackman.a and runs the tests.
86+
make abiname
87+
make PLATFORMFLAGS=-m32 test
88+
make PLATFORM_PREFIX=arm-linux-gnueabi- EMULATOR=qemu-arm test
89+
```
90+
91+
`make clean` only when switching `PLATFORMFLAGS` or `PLATFORM_PREFIX`.
92+
Success ends with `*** All test suites passed ***`.
93+
94+
Four programs: `bin/test`, `bin/test_cc`, `bin/test_static`,
95+
`bin/test_asm`. `tests/test.c` is `test_01` through `test_08`. `test_04`
96+
and `test_05` are the `stackman_call` contract. `test_07` and `test_08`
97+
pressure the compiler. They pass even when a callee-saved register is
98+
dropped, if the compiler kept the values in memory.
99+
100+
The linker warning `missing .note.GNU-stack section` on
101+
`tests/test_asm_s.o` is expected.
102+
103+
Intel CET: the Makefile adds `-fcf-protection=none` through
104+
`./disable_cet`. Stack switching is incompatible with the shadow stack.
105+
Regenerating asm uses the same flag (`gcc -S -fcf-protection=none`).
106+
107+
Windows, as CI runs it: `msbuild vs2022\stackman.sln /p:Platform=x64`
108+
(also `x86` and `arm64`). Native tests are `vs2022\x64\Debug\test.exe`
109+
and `vs2022\Win32\Debug\test.exe`. The arm64 build is not run. After
110+
`tools/strip-lib.py`, CI rebuilds.
111+
112+
Do not commit rebuilt `lib/` archives, `*.o`, `bin/`, or `tmp/`.
113+
Checked-in libraries under `lib/` are deprecated and go away in a future
114+
major version. `commit-artifacts` in
115+
`.github/workflows/buildcommit.yml` is `if: false`.
116+
117+
## CI
118+
119+
`.github/workflows/buildcommit.yml`, on pull requests to `master`:
120+
121+
- `build-linux-gnu`: AMD64, i386 (`-m32`), arm (`qemu-arm`), aarch64
122+
(`qemu-aarch64`), riscv64 (`qemu-riscv64`). Each runs `make test`.
123+
- `build-macos`: `macos-15-intel` (`darwin_x86_64`) and `macos-latest`
124+
(`darwin_arm64`).
125+
- `build-windows`: VS2022 `x86`, `x64`, and `arm64`. Tests run only
126+
where `native` is set (`x86` and `x64`).
127+
128+
`.github/workflows/buildbsd.yml` builds and tests FreeBSD, OpenBSD, and
129+
NetBSD with `gmake` on the `sysv_amd64` sources.
130+
131+
A green native `make test` says nothing about another ABI. An assembly
132+
change is checked by the job that builds that file.
133+
134+
## Edits
135+
136+
Keep changes small. Do not add a dependency. Do not bump `stackman.h` or
137+
`CHANGELOG.md` unless asked.
138+
139+
Prefer the checked-in assembly over turning `STACKMAN_INLINE_ASM` on.
140+
`switch_template.h` is the skeleton for a new port.
141+
142+
There is no `main` branch. `master` is the default on both `origin`
143+
(`kristjanvalur/stackman`) and `upstream` (`stackless-dev/stackman`).

‎README.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ platforms so that no assembly steps are required by users.
5959
straightforward implementation on most platforms.
6060
- Complex logic and branching is delegated to the C callback.
6161
- Custom platform assembly code must only do three things:
62-
1. Save and restore volatile registers and stack state on the stack
62+
1. Save and restore callee-saved (non-volatile) registers and stack state on the stack
6363
2. Call the callback twice with the current stack pointer
6464
3. Set the stack pointer to the value returned by the first callback.
6565

@@ -105,6 +105,9 @@ calling convention, plus archive format:
105105
- **FreeBSD, OpenBSD, NetBSD (System V ABI)**
106106
- sysv_amd64 (64-bit x86_64)
107107

108+
The 64-bit ARM ABIs (`aarch64`, `darwin_arm64`, and `win_arm64`) do not
109+
preserve SME ZA context across `stackman_switch`.
110+
108111
All platforms are automatically built and tested by GitHub Actions CI on every commit.
109112

110113
The BSDs share the `sysv_amd64` ABI with Linux and therefore need no separate
@@ -157,7 +160,9 @@ use.
157160
158161
1. It saves the current CPU stack pointer. If `stack` is non-zero, it will replace the stackpointer
159162
with that value.
160-
2. It calls the callback function with the opcode `STACKMAN_OP_CALL`.
163+
2. It calls the callback function with the opcode `STACKMAN_OP_CALL`. The
164+
stack pointer argument is the previous stack pointer, the one in use before
165+
this call. A null `stack` leaves that pointer unchanged.
161166
3. It replaces the stack pointer with the previously saved value and returns the return value from the callback.
162167
163168
This function is useful for at least three things:

0 commit comments

Comments
 (0)