From 0fa9a2f7e04c3b78b27d85c05dd86f4ef7975701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Thu, 15 Oct 2026 15:44:19 +0000 Subject: [PATCH 1/6] Release v1.3.0 Bump STACKMAN_VERSION and the README banner, and record the changes since 1.2.5 in the changelog. The release tag is not created here. --- CHANGELOG.md | 27 +++++++++++++++++++++++++-- README.md | 2 +- stackman/stackman.h | 6 +++--- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49f2c42..e234cb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.0] - 2026-10-15 + ### Added -- FreeBSD, OpenBSD and NetBSD (amd64, `sysv_amd64` ABI) are built and tested on every commit by a new `build and test bsd` workflow. +- `armhf`, a 32-bit ARM hard-float ABI (AAPCS-VFP). `__ARM_PCS_VFP` selects it. Soft-float stays `arm32`. CI and the release archive include `lib/armhf`. +- `docs/platform-abi.md`, the current contract for switching and register spills. `docs/abi-register-audit.md` remains the historical record of that audit. +- `test_09` checks that the caller's rounding mode survives `stackman_switch`, including a switch onto a saved stack. Soft-float `arm32` does not spill `FPSCR`, and the test expects the callback's mode to remain. +- FreeBSD, OpenBSD and NetBSD (amd64, `sysv_amd64` ABI) are built and tested on every commit by a `build and test bsd` workflow. + +### Changed +- `stackman_switch` spills floating-point control and status state and reloads it after the restore callback. `stackman_call` does not touch those registers. +- `armhf` spills the whole `FPSCR`. +- `aarch64` and `darwin_arm64` spill `FPCR` and `FPSR`. +- `win_arm64` spills `FPSR` as well as `FPCR`. +- `riscv64` spills the whole `fcsr`. +- `sysv_i386` spills the x87 control word. `win_x64` spills `MXCSR`. +- `win_x86` spills the x87 control word and `MXCSR`, so older compilers that emit x87 and current MSVC, which emits SSE2, each keep their rounding mode. +- Softfp (`-mfloat-abi=softfp`) is not a supported platform. That build selects `arm32` and does not preserve `FPSCR`. +- CI test steps stop after one minute. ### Fixed +- RISC-V `stackman_call` leaves a null stack unchanged and passes the previous stack pointer to the callback. +- Windows ARM64 no longer uses the live TEB register `x18` as a scratch in `stackman_switch` or `stackman_call`. +- AArch64 `stackman_switch` keeps the frame pointer attached across a stack switch, and the unwind info covers the `FPCR` slot. - ABI detection now works on the BSDs: `tools/abiname.sh` used a `mktemp` template with three `X`s, which BSD `mktemp(1)` rejects. +### Removed +- Leftover Windows ARM32 sources, Visual Studio configurations, and `lib/win_arm`. Version 1.0.0 had already dropped that platform. + ## [1.2.5] - 2026-06-23 ### Changed @@ -142,7 +164,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - GitHub Actions CI for automated building and testing - Visual Studio project files (VS2017, VS2019, VS2022) -[Unreleased]: https://github.com/stackless-dev/stackman/compare/v1.2.5...HEAD +[Unreleased]: https://github.com/stackless-dev/stackman/compare/v1.3.0...HEAD +[1.3.0]: https://github.com/stackless-dev/stackman/releases/tag/v1.3.0 [1.2.5]: https://github.com/stackless-dev/stackman/releases/tag/v1.2.5 [1.2.4]: https://github.com/stackless-dev/stackman/releases/tag/v1.2.4 [1.2.3]: https://github.com/stackless-dev/stackman/releases/tag/v1.2.3 diff --git a/README.md b/README.md index 2d74611..999c87e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ║ ║ ║ Do you have a stack? Stackman is here to help. ║ ║ ║ -║ v1.2.5 • 11 platforms • Stack switching simplified ║ +║ v1.3.0 • 11 platforms • Stack switching simplified ║ ║ ║ ╚══════════════════════════════════════════════════════════════╝ ``` diff --git a/stackman/stackman.h b/stackman/stackman.h index 5a0d7b1..47e2f55 100644 --- a/stackman/stackman.h +++ b/stackman/stackman.h @@ -4,11 +4,11 @@ /* Version information */ #define STACKMAN_VERSION_MAJOR 1 -#define STACKMAN_VERSION_MINOR 2 -#define STACKMAN_VERSION_PATCH 5 +#define STACKMAN_VERSION_MINOR 3 +#define STACKMAN_VERSION_PATCH 0 /* Version as a string */ -#define STACKMAN_VERSION "1.2.5" +#define STACKMAN_VERSION "1.3.0" /* Version as a single number for comparisons (MMmmpp: Major, minor, patch) */ #define STACKMAN_VERSION_NUMBER ((STACKMAN_VERSION_MAJOR * 10000) + \ From 183747b1783597856fe5832409e05d69e8ef016d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Thu, 15 Oct 2026 15:59:00 +0000 Subject: [PATCH 2/6] Report the Windows test fault instead of a bare exit code. The x64 test.exe run exits 1 with no assertion text and no test names. stdout is fully buffered on that pipe, and PowerShell turns any native failure into exit code 1. The test now prints each name unbuffered and logs the exception code. The Windows step runs under cmd and prints that process exit code. --- .github/workflows/buildcommit.yml | 7 +++- tests/test.c | 56 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/.github/workflows/buildcommit.yml b/.github/workflows/buildcommit.yml index 0fe9f48..07c1348 100644 --- a/.github/workflows/buildcommit.yml +++ b/.github/workflows/buildcommit.yml @@ -163,7 +163,12 @@ jobs: - name: test if: ${{ matrix.native == 'yes' }} timeout-minutes: 1 - run: vs2022\${{matrix.folder}}\Debug\test.exe + shell: cmd + run: | + vs2022\${{matrix.folder}}\Debug\test.exe + set RC=%ERRORLEVEL% + echo test.exe exit %RC% + exit /b %RC% - name: Upload build artifacts uses: actions/upload-artifact@v7.0.0 with: diff --git a/tests/test.c b/tests/test.c index 9a16401..248b92c 100644 --- a/tests/test.c +++ b/tests/test.c @@ -7,6 +7,51 @@ #include #include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include + +/* Name of the test currently running, so a fault names itself. + * stdout from a crashed process is lost when it is fully buffered. */ +static const char *current_test; + +static LONG WINAPI test_crash(EXCEPTION_POINTERS *info) +{ + EXCEPTION_RECORD *rec = info->ExceptionRecord; + + fprintf(stderr, "exception 0x%08lx at %p during %s", + rec->ExceptionCode, rec->ExceptionAddress, + current_test ? current_test : "startup"); + if (rec->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && + rec->NumberParameters >= 2) { + fprintf(stderr, " %s %p", + rec->ExceptionInformation[0] ? "write" : "read", + (void *)rec->ExceptionInformation[1]); + } + fprintf(stderr, "\n"); + fflush(stderr); + return EXCEPTION_CONTINUE_SEARCH; +} +#endif + +static void test_startup(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); +#ifdef _WIN32 + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + AddVectoredExceptionHandler(1, test_crash); +#endif +} + +static void test_enter(const char *name) +{ +#ifdef _WIN32 + current_test = name; +#endif + printf("%s\n", name); +} + /* Soft-float arm32 does not spill FPSCR. Softfp selects that assembly. */ #if defined(__arm__) && !defined(__aarch64__) && !defined(__ARM_PCS_VFP) #define STACKMAN_ROUND_ISOLATED 0 @@ -567,25 +612,36 @@ void test_09(void *stack_marker) int main(int argc, char*argv[]) { int stack_marker = 0; + + test_startup(); + test_enter("test_01"); test_01(); printf("test_01 ok\n"); + test_enter("test_02"); test_02((void*)&stack_marker); printf("test_02 ok\n"); + test_enter("test_03"); test_03((void*)&stack_marker); printf("test_03 ok\n"); #ifdef TEST_04 + test_enter("test_04"); test_04(); printf("test_04 ok\n"); + test_enter("test_05"); test_05(); printf("test_05 ok\n"); #endif + test_enter("test_06"); test_06(); printf("test_06 ok\n"); + test_enter("test_07"); test_07(); printf("test_07 ok\n"); + test_enter("test_08"); test_08(); printf("test_08 ok\n"); + test_enter("test_09"); test_09((void*)&stack_marker); printf("test_09 ok\n"); return 0; From c6fa6c657e191df8dd01520f9a2c2270a99f1b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Thu, 15 Oct 2026 18:13:33 +0000 Subject: [PATCH 3/6] Note the x64 test stack fix in the 1.3.0 changelog. The stack enlargement itself is on master. This records it with the other changes since 1.2.5. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e234cb9..45aae06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CI test steps stop after one minute. ### Fixed +- `test_04` allocates 8KB for the `stackman_call` stack. Two MSVC Debug (`/ZI`) frames of the recursive callback overflowed a 1KB block into the heap header, so the x64 test failed intermittently. - RISC-V `stackman_call` leaves a null stack unchanged and passes the previous stack pointer to the callback. - Windows ARM64 no longer uses the live TEB register `x18` as a scratch in `stackman_switch` or `stackman_call`. - AArch64 `stackman_switch` keeps the frame pointer attached across a stack switch, and the unwind info covers the `FPCR` slot. From 1529605698ce7fa8e91afd1f0273c29eb350bd61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Thu, 15 Oct 2026 19:52:46 +0000 Subject: [PATCH 4/6] Document the Scalable Matrix Extension and how a switch keeps ZA. The note covers where FEAT_SME shows up, the off, active, and dormant modes, and the helper sequence for aarch64 and darwin_arm64. --- docs/scalable-matrix-extension.md | 142 ++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 docs/scalable-matrix-extension.md diff --git a/docs/scalable-matrix-extension.md b/docs/scalable-matrix-extension.md new file mode 100644 index 0000000..224bfca --- /dev/null +++ b/docs/scalable-matrix-extension.md @@ -0,0 +1,142 @@ +# Scalable Matrix Extension + +SME is the Scalable Matrix Extension. It is an optional AArch64 feature, +`FEAT_SME`, published by Arm in 2021 as part of the Armv9-A architecture. +This note is the plan for keeping its ZA state across `stackman_switch`. +The register spills for the other ABIs stay in `platform-abi.md`. + +## Where it comes from + +SME is a CPU feature, not an operating-system feature and not a baseline +part of AArch64. A core either implements `FEAT_SME` or it does not. Armv8-A +machines do not have it. Plenty of Armv9-A machines do not have it either. +Cores that do include Apple M4 and later, and some Neoverse and Cortex +parts from that generation. The streaming vector length is chosen by the +implementation. ZA is one square matrix of that length: from 256 bytes at +the shortest length up to 64 KB at the longest. + +Software sees it through the Arm Procedure Call Standard for the 64-bit +Architecture (AAPCS64). That document defines the ZA lazy-save scheme and +the runtime helpers (`__arm_sme_state`, `__arm_za_disable`, +`__arm_tpidr2_restore`, and the others). The helpers live in the compiler +runtime, libgcc or compiler-rt, on toolchains that implement the scheme. +They ask the platform whether this thread may use SME, and they return +without touching ZA when it may not. A library built for plain `armv8-a` +can call them. + +Linux and macOS on AArch64 use that AAPCS64 runtime. In this tree those +are the `aarch64` and `darwin_arm64` ABIs, both built from +`switch_aarch64_gcc.S`. Windows on ARM64 does not provide these helpers +and does not use this lazy-save scheme. `win_arm64` is out of scope here. +`stackman_call` returns to the same caller, so the caller's buffer stays +on the live stack and this scheme is not required there. + +SVE, the Scalable Vector Extension, widens the vector registers. Advanced +SIMD is the 128-bit `v0`–`v31` file. ZA is neither of those. It is a +separate matrix accumulator with its own enable bit. + +## The three modes + +ZA is in exactly one of these modes. The names are the AAPCS64 ones. +`PSTATE.ZA` is the enable bit. `TPIDR2_EL0` is a system register that +either holds null or points at a 16-byte TPIDR2 block on some function's +stack. Byte 0 of that block is `za_save_buffer`, the address of the rows. +Bytes 8–9 are `num_za_save_slices`. The matrix is not stored at the +pointer itself. + +**Off.** `PSTATE.ZA` is 0 and `TPIDR2_EL0` is null. The matrix is disabled +and holds nothing that matters. `__arm_za_disable` leaves this mode +behind. This is the disabled state. + +**Active.** `PSTATE.ZA` is 1 and `TPIDR2_EL0` is null. The matrix is on +and the bytes sit in the hardware register. The function that is running +is using ZA directly. There is no buffer waiting to receive a copy. + +**Dormant.** `PSTATE.ZA` is 1 and `TPIDR2_EL0` points at a TPIDR2 block. +The matrix is still on, and the bytes are still in the hardware register. +The block says where to copy them if some other function needs the +register. The buffer is stale until that copy happens. Dormant exists +because the matrix is large. A normal call does not copy it. The owner +sets the pointer, makes the call, and on return either clears the pointer +(the callee never needed ZA, so the register still holds the bytes) or +finds the pointer already cleared (the callee committed a copy, and the +owner loads the buffer back). + +Active and dormant both have the matrix turned on. The difference is the +pointer. Off is the only mode in which the matrix is disabled. + +`stackman_switch` is an ordinary C function. The procedure call standard +classifies every such function as private-ZA. On entry, ZA is off or +dormant. It is not active. A caller that was using ZA switches from +active to dormant before the call, by pointing `TPIDR2_EL0` at a block in +its own frame. At the first instruction of `stackman_switch` the bytes are +still in the hardware register. + +## How a switch keeps that state + +The goal is to put back the mode `stackman_switch` found on entry, for +whichever stack is about to run. Off stays off. Dormant becomes dormant +again, with the same block pointer and the same bytes in the register. + +Do this with the AAPCS64 helpers, not with `__arm_sme_save` and +`__arm_sme_restore`. That second pair snapshots active ZA into a private +buffer and does nothing when `TPIDR2_EL0` is already set. The mode on +entry is dormant or off, so the pair would not see it. + +Saving the outgoing stack happens before the `STACKMAN_OP_SAVE` callback. +That callback is allowed to copy or abandon the stack, and a dormant +`TPIDR2_EL0` points into it. + +1. Call `__arm_sme_state`. It is safe on a machine without SME. The + TPIDR2 value comes back in `x1`. Do not read `TPIDR2_EL0` with `mrs`. + That register traps when the thread has no access to it. +2. If the pointer is null, the mode is off. Store a null word in this + stack's spill image. +3. If the pointer is non-null, the mode is dormant. Keep the pointer, + then call `__arm_za_disable`. The helper copies the live rows into + `za_save_buffer`, clears `TPIDR2_EL0`, and turns ZA off. Store the + saved pointer in the spill image. + +The spill slot has to sit at or above the stack pointer that the callback +sees, or a copy of the stack will not carry it. The published pointer +stays the `FPCR`/`FPSR` slot. The block pointer is the next 8 bytes, with +8 bytes of padding so the slot stays 16-byte aligned: + +| Offset from the published stack pointer | Contents | +| --- | --- | +| 0 | `FPCR` | +| 8 | `FPSR` | +| 16 | saved TPIDR2 block pointer, or 0 | +| 24 | 0 | + +Restoring the incoming stack happens after its stack pointer is installed +and before the `STACKMAN_OP_RESTORE` callback. Code on that stack then +sees the mode its own `stackman_switch` entry had. + +1. Load the saved pointer from that stack's slot. +2. A null pointer means off. Leave ZA disabled and leave `TPIDR2_EL0` + null. +3. A non-null pointer means dormant. `smstart za` turns the matrix on + without changing streaming mode (`PSTATE.SM` is already 0 on entry to + this non-streaming function). Call `__arm_tpidr2_restore` with the + saved block pointer. That helper requires `TPIDR2_EL0` to be null and + ZA to be on, and it only copies the rows. Then write the pointer back + to `TPIDR2_EL0`. The mode is dormant again. + +The same steps run when the callback does not switch. Entry was dormant, +the save committed it, and the restore puts dormant back before +`stackman_switch` returns. The caller's compiler then sees its own block +pointer still in `TPIDR2_EL0` and keeps the register contents. + +Link the three helpers as weak symbols. A toolchain that does not ship +them resolves the addresses to zero. In that case store a null pointer +and skip both calls, which is the behaviour this tree has today. If +`__arm_tpidr2_restore` is missing, do not call `__arm_za_disable` either: +committing without a way to load the buffer back would drop the rows. +Windows ARM64 does not get these calls. + +Two limits belong with the mechanism. The saved pointer and +`za_save_buffer` are absolute addresses. A callback that copies a stack +to a new base has to relocate them, the same way it already has to +relocate saved frame pointers. `ZT0`, the SME2 lookup table, is +caller-saved and is not part of this state. From 187af8fb2bb977ec021c83999c6956b8486a0fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Thu, 15 Oct 2026 19:58:24 +0000 Subject: [PATCH 5/6] Save and restore dormant SME ZA across the AArch64 switch. stackman_sme_suspend commits a dormant ZA before the save callback and parks the TPIDR2 block pointer next to FPCR. stackman_sme_resume puts that dormant state back after the new stack is installed. Off stays off. The helpers are weak, and Windows ARM64 is unchanged. --- AGENTS.md | 14 +-- README.md | 5 +- docs/platform-abi.md | 15 ++-- docs/scalable-matrix-extension.md | 3 +- stackman/platforms/switch_aarch64_gcc.S | 40 +++++++-- stackman/platforms/switch_aarch64_gcc.h | 115 +++++++++++++++++++++--- 6 files changed, 157 insertions(+), 35 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 22f404e..bf7597d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,12 +66,14 @@ Limits that are easy to fix the wrong way: (`-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 - 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. +- `aarch64` and `darwin_arm64` keep dormant or off SME ZA across + `stackman_switch`. The scheme is `docs/scalable-matrix-extension.md`. + `stackman_sme_suspend` runs before the save callback and + `stackman_sme_resume` runs after the new stack pointer is installed. + Do not restore ZA with `__arm_sme_save` / `__arm_sme_restore`. Do not + read `TPIDR2_EL0` with `mrs`. `stackman_call` does not touch ZA. + Windows ARM64 is a different runtime. Do not call the AAPCS64 helpers + there, and do not pretend `win_arm64` preserves ZA. - 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. diff --git a/README.md b/README.md index 999c87e..57ca7cd 100644 --- a/README.md +++ b/README.md @@ -109,8 +109,9 @@ calling convention, plus archive format: 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`. +`aarch64` and `darwin_arm64` preserve a dormant or off SME ZA state +across `stackman_switch`. `win_arm64` does not. See +[`docs/scalable-matrix-extension.md`](docs/scalable-matrix-extension.md). 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 diff --git a/docs/platform-abi.md b/docs/platform-abi.md index 3d3ddb3..120dce8 100644 --- a/docs/platform-abi.md +++ b/docs/platform-abi.md @@ -104,15 +104,20 @@ for the same reason as `arm32`. 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 +`FPCR` and `FPSR` are stored in a 32-byte slot at the pointer passed to +the callback and written back after `STACKMAN_OP_RESTORE`. The slot is +`FPCR`, `FPSR`, the saved TPIDR2 block pointer, and 8 bytes of padding. +A null pointer means ZA was off on entry. A non-null pointer is a +dormant ZA state. `stackman_sme_suspend` commits it with +`__arm_za_disable` before the save callback, and `stackman_sme_resume` +restores it after the new stack pointer is installed, before the +restore callback. The helpers are weak. See +[`scalable-matrix-extension.md`](scalable-matrix-extension.md). `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`. +`stackman_call` does not spill `FPCR`, `FPSR`, `d8`–`d15`, or ZA. ### `win_arm64` diff --git a/docs/scalable-matrix-extension.md b/docs/scalable-matrix-extension.md index 224bfca..8098a3a 100644 --- a/docs/scalable-matrix-extension.md +++ b/docs/scalable-matrix-extension.md @@ -2,7 +2,8 @@ SME is the Scalable Matrix Extension. It is an optional AArch64 feature, `FEAT_SME`, published by Arm in 2021 as part of the Armv9-A architecture. -This note is the plan for keeping its ZA state across `stackman_switch`. +This note is how `stackman_switch` keeps its ZA state on `aarch64` and +`darwin_arm64`. The register spills for the other ABIs stay in `platform-abi.md`. ## Where it comes from diff --git a/stackman/platforms/switch_aarch64_gcc.S b/stackman/platforms/switch_aarch64_gcc.S index 208610d..4551629 100644 --- a/stackman/platforms/switch_aarch64_gcc.S +++ b/stackman/platforms/switch_aarch64_gcc.S @@ -34,6 +34,14 @@ #define CFI_DEF_CFA_OFFSET(x) .cfi_def_cfa_offset x #define CFI_OFFSET(r,o) .cfi_offset r, o #define CFI_RESTORE(r) .cfi_restore r +#endif + +#ifdef __APPLE__ +#define SME_SUSPEND _stackman_sme_suspend +#define SME_RESUME _stackman_sme_resume +#else +#define SME_SUSPEND stackman_sme_suspend +#define SME_RESUME stackman_sme_resume #endif .arch armv8-a @@ -78,22 +86,32 @@ LABEL(stackman_switch) CFI_OFFSET(79,-8) mov x3, x0 mov x0, x1 + /* x19 and x20 are already spilled. Hold the callback and the + * context across stackman_sme_suspend. It is a normal call. + */ + mov x19, x3 + mov x20, x0 + bl SME_SUSPEND + mov x4, x0 + mov x0, x20 + mov x3, x19 #APP // 70 "../platforms/switch_aarch64_gcc.h" 1 mrs x1, fpcr mrs x2, fpsr - stp x1, x2, [sp, #-16]! + stp x1, x2, [sp, #-32]! + stp x4, xzr, [sp, #16] // 0 "" 2 // 85 "../platforms/switch_aarch64_gcc.h" 1 mov x2, sp // 0 "" 2 #NO_APP - /* The 16-byte FPCR/FPSR slot is outside the compiler frame. - * Keep it out of the #APP block: Darwin assembles that block, - * and these macros are empty there. A gcc -S regen does not - * emit this, because the compiler does not see the sp write. + /* FPCR/FPSR and the saved TPIDR2 pointer sit below the compiler + * frame. The published sp is the FPCR word. Darwin assembles the + * #APP block with no .cfi_startproc, so the CFA for this slot is + * set here. A gcc -S regen does not emit it. */ - CFI_DEF_CFA_OFFSET(176) + CFI_DEF_CFA_OFFSET(192) mov w1, 0 mov x20, x0 mov x19, x3 @@ -106,12 +124,20 @@ LABEL(stackman_switch) sub x29, x29, x1 // 0 "" 2 #NO_APP + /* Null means ZA was off on entry to this stack's switch. + * A pointer means dormant: turn ZA on, copy the rows back, and + * point TPIDR2_EL0 at the same block. x19 and x20 stay live. + */ + ldr x0, [sp, #16] + cbz x0, .Lsme_resume_skip + bl SME_RESUME +.Lsme_resume_skip: mov w1, 1 mov x0, x20 blr x19 #APP // 102 "../platforms/switch_aarch64_gcc.h" 1 - ldp x1, x2, [sp], #16 + ldp x1, x2, [sp], #32 msr fpcr, x1 msr fpsr, x2 // 0 "" 2 diff --git a/stackman/platforms/switch_aarch64_gcc.h b/stackman/platforms/switch_aarch64_gcc.h index 9058c28..957a91c 100644 --- a/stackman/platforms/switch_aarch64_gcc.h +++ b/stackman/platforms/switch_aarch64_gcc.h @@ -8,6 +8,11 @@ * including the caller-saved status bits. stackman_call does not * change them and does not spill them. * + * The same slot carries the caller's TPIDR2 block pointer so a dormant + * SME ZA state can be committed on the way out and restored on the way + * back in. See docs/scalable-matrix-extension.md. stackman_call does + * not touch it: that call returns to the same caller. + * */ /* clang cannot perform inline assembly using specific __attr__ * instructions, and so it may use a base pointer and other @@ -25,6 +30,73 @@ #endif #ifdef STACKMAN_SWITCH_IMPL + +#if !__ASSEMBLER__ +/* + * Called from switch_aarch64_gcc.S and from the inline copy below. + * Weak on purpose: a toolchain without the AAPCS64 SME runtime resolves + * these to zero, and the switch stores a null pointer. + * + * target("arch=armv8-a") keeps an SME-aware compiler from wrapping the + * helpers in a lazy save of its own. smstart za is encoded with .inst so + * this file still assembles for plain armv8-a. 0xd503457f is "smstart za". + * S3_3_C13_C0_5 is TPIDR2_EL0. + */ +extern void __arm_za_disable(void) __attribute__((weak)); +extern void __arm_tpidr2_restore(void *) __attribute__((weak)); + +typedef struct { + unsigned long long status; + void *tpidr2; +} stackman_sme_state_result; + +extern stackman_sme_state_result __arm_sme_state(void) __attribute__((weak)); + +__attribute__((visibility("hidden"), noinline, target("arch=armv8-a"))) +void *stackman_sme_suspend(void); + +__attribute__((visibility("hidden"), noinline, target("arch=armv8-a"))) +void stackman_sme_resume(void *tpidr2); + +void *stackman_sme_suspend(void) +{ + stackman_sme_state_result state; + + if (!__arm_sme_state || !__arm_za_disable || !__arm_tpidr2_restore) + return 0; + state = __arm_sme_state(); + if (!state.tpidr2) + return 0; + __arm_za_disable(); + return state.tpidr2; +} + +void stackman_sme_resume(void *tpidr2) +{ + if (!tpidr2 || !__arm_tpidr2_restore) + return; + { + /* x0 survives __arm_tpidr2_restore. The helper preserves it. */ + register void *blk asm("x0") = tpidr2; + register void (*fn)(void *) asm("x16") = __arm_tpidr2_restore; + + __asm__ volatile( + ".inst 0xd503457f\n\t" + "blr %1\n\t" + "msr S3_3_C13_C0_5, %0\n\t" + : "+r"(blk) + : "r"(fn) + : "x14", "x15", "x17", "x30", "memory", "cc"); + } +} + +#if defined(__APPLE__) +#define STACKMAN_SME_RESUME "bl _stackman_sme_resume\n\t" +#else +#define STACKMAN_SME_RESUME "bl stackman_sme_resume\n\t" +#endif +#endif /* !__ASSEMBLER__ */ + #if !__ASSEMBLER__ && !defined(STACKMAN_ASSEMBLY_SRC) /* @@ -55,23 +127,26 @@ STACKMAN_LINKAGE_SWITCH_INASM void *STACKMAN_SWITCH_INASM_NAME(stackman_cb_t callback, void *context) { void *sp; + void *tpidr2; /* have gcc save/restore registers for us on the stack */ __asm__ volatile ("" : : : GP_PRESERVE); #ifdef FP_PRESERVE __asm__ volatile ("" : : : FP_PRESERVE); #endif /* FPCR and FPSR are one thread context on Linux and macOS. - * Park both in a 16-byte slot and pass that sp to the callback, - * so the copied image carries them. Sixteen bytes keeps the - * AAPCS64 alignment. x1 and x2 are free until the compiler - * assigns the saved stack pointer. The reload clobbers the - * same registers after the restore callback returns. + * Park both in the bottom of a 32-byte slot and pass that sp + * to the callback, so the copied image carries them. The next + * word is the TPIDR2 block pointer just captured, or null when + * ZA was off or the SME helpers are absent. x1 and x2 are free + * until the compiler assigns the saved stack pointer. */ + tpidr2 = stackman_sme_suspend(); __asm__ volatile ( "mrs x1, fpcr\n\t" "mrs x2, fpsr\n\t" - "stp x1, x2, [sp, #-16]!" - ::: "x1", "x2", "memory"); + "stp x1, x2, [sp, #-32]!\n\t" + "stp %0, xzr, [sp, #16]" + :: "r"(tpidr2) : "x1", "x2", "memory"); /* The compiler's CFI still describes its own frame. This * adjusts by the slot just pushed. It is not in the stp asm: * that string is copied into the shared .S, and Darwin @@ -79,33 +154,45 @@ void *STACKMAN_SWITCH_INASM_NAME(stackman_cb_t callback, void *context) * CFA with CFI_DEF_CFA_OFFSET instead. */ #if !defined(__APPLE__) - __asm__ volatile (".cfi_adjust_cfa_offset 16"); + __asm__ volatile (".cfi_adjust_cfa_offset 32"); #endif /* sp = get stack pointer from assembly code */ __asm__ ("mov %[result], sp" : [result] "=r" (sp)); /* store stack */ sp = callback(context, STACKMAN_OP_SAVE, 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. + * Resume a dormant ZA before the callback runs on this stack. + * A null slot means the entry mode was off. The call is inside + * this asm so the compiler does not spill between the sp write + * and the restore. */ - __asm__ ( + __asm__ volatile ( "sub x1, sp, %[result]\n\t" "mov sp, %[result]\n\t" - "sub x29, x29, x1" - : : [result] "r" (sp) : "x1"); + "sub x29, x29, x1\n\t" + "ldr x0, [sp, #16]\n\t" + "cbz x0, .Lsme%=\n\t" + STACKMAN_SME_RESUME + ".Lsme%=:\n\t" + : : [result] "r" (sp) + : "x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", + "x8", "x9", "x10", "x11", "x12", "x13", "x14", "x15", + "x16", "x17", "x30", + "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", + "memory", "cc"); sp = callback(context, STACKMAN_OP_RESTORE, sp); /* Reload FPCR and FPSR from the stack now current, then release * the slot before the epilogue pops the callee-saved registers. */ __asm__ volatile ( - "ldp x1, x2, [sp], #16\n\t" + "ldp x1, x2, [sp], #32\n\t" "msr fpcr, x1\n\t" "msr fpsr, x2" ::: "x1", "x2", "memory"); #if !defined(__APPLE__) - __asm__ volatile (".cfi_adjust_cfa_offset -16"); + __asm__ volatile (".cfi_adjust_cfa_offset -32"); #endif return sp; } From b152f03748efb6eebb2e9deafb8ced8d5e820578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Thu, 15 Oct 2026 22:04:01 +0000 Subject: [PATCH 6/6] Reload the stack pointer after the SME resume call. stackman_sme_resume is a normal call, so it can destroy x2. The restore callback takes the installed stack pointer in that register. The null path skipped the call and kept the pointer. The dormant path did not. --- stackman/platforms/switch_aarch64_gcc.S | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stackman/platforms/switch_aarch64_gcc.S b/stackman/platforms/switch_aarch64_gcc.S index 4551629..b3e894a 100644 --- a/stackman/platforms/switch_aarch64_gcc.S +++ b/stackman/platforms/switch_aarch64_gcc.S @@ -132,6 +132,11 @@ LABEL(stackman_switch) cbz x0, .Lsme_resume_skip bl SME_RESUME .Lsme_resume_skip: + /* stackman_sme_resume is a normal call, so x2 does not + * survive it. The restore callback's third argument is the + * stack just installed. Reload that on both paths. + */ + mov x2, sp mov w1, 1 mov x0, x20 blr x19