From 313da1d257f8e902f926f2c84c575b5bf7c2458e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 08:25:44 +0000 Subject: [PATCH] libap: ucontext's three switches must be assembly, not C wrappers The VM refuted the C-wrapper design on the first run. ucontext-test section 0 printed its PASS line once instead of twice and reported `setcontext came back round: got 1 want 2'. What a C getcontext saves is a point inside getcontext itself. That is fine until the context is resumed after getcontext has returned -- by then the wrapper's frame is dead stack, its own return-address slot has been reused by whatever the caller did next, and the resumed epilogue RETs to that. Here it landed after the setcontext CALL, because setcontext's return address was sitting in the slot, so control appeared to flow straight on with the counter still at 1. The same reasoning says why swapcontext got away with it, and that half is worth keeping so the lesson is not over-general: a context saved by swapcontext is resumed while its stack is parked, so nothing has run on it and the frame is intact. Sections 1 to 4 all passed. getcontext is the one that returns and is then resumed, and the only one that could have shown this. All three are assembly now, which is what setjmp.s was all along, and the files are named for what they hold: ucontext.s and makecontext.c. The cost is one shared offset, uc_sigmask at 64, and makecontext.c turns a change to it into a compile error rather than a switch that quietly restores the wrong mask -- verified by swapping two members and watching `size of array _uc_layout_assert is negative'. The mask is handled as sigsetjmp handles it, through _psigblocked(SB), with MOVL because sigset_t is long and kencc's long is four bytes. Section 5's failure was the test's own, and of the one kind gcc cannot catch: `volatile long a = 0x1111111111111111L' holds 0x11111111 here, so the comparison against the full constant was false and a working switch reported got 0 want 1. long long now. The glibc run could not have found it, because the difference IS the reference's width. makectx-xcheck follows the rename and stubs setcontext instead of the three primitives; its two frame controls still give 3 and 1. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WGAwvvTwDg2yknFkmZ3qzs --- docs/notes/libap.md | 47 +++++- sys/include/ape/ucontext.h | 19 ++- sys/lib/tests/makectx-xcheck.c | 48 +++--- sys/lib/tests/makectx-xcheck.sh | 16 +- sys/lib/tests/ucontext-test.c | 19 ++- .../arch/amd64/{ucontext.c => makecontext.c} | 96 ++++-------- sys/src/ape/lib/ap/arch/amd64/mcontext.s | 128 ---------------- sys/src/ape/lib/ap/arch/amd64/mkfile | 2 +- sys/src/ape/lib/ap/arch/amd64/ucontext.s | 145 ++++++++++++++++++ 9 files changed, 275 insertions(+), 245 deletions(-) rename sys/src/ape/lib/ap/arch/amd64/{ucontext.c => makecontext.c} (66%) delete mode 100644 sys/src/ape/lib/ap/arch/amd64/mcontext.s create mode 100644 sys/src/ape/lib/ap/arch/amd64/ucontext.s diff --git a/docs/notes/libap.md b/docs/notes/libap.md index 444b59060..46ec67ef8 100644 --- a/docs/notes/libap.md +++ b/docs/notes/libap.md @@ -4055,9 +4055,50 @@ means in a host program. *The instrument reporting itself is the shape to recognise rather than argue with* -- the assertion was right and the fake was wrong. -**What neither covers**: `uc_sigmask` is saved and restored through -`sigprocmask`, which on APE is a read and a write of one global -(`signal/sigprocmask.c`), and nothing here exercises a context switch +### The C wrapper could not save its caller's frame + +***THE FIRST VERSION HAD `getcontext`, `setcontext` AND `swapcontext` +AS C AROUND THREE REGISTER-ONLY PRIMITIVES, AND THE VM REFUTED IT.*** +`ucontext-test` section 0 printed its PASS line **once** instead of +twice and reported `setcontext came back round: got 1 want 2`. + +What a C `getcontext` saves is a point inside `getcontext` itself. +That is fine until the context is RESUMED after `getcontext` has +returned -- by then the wrapper's frame is dead stack, its own +return-address slot has been reused by whatever the caller did next, +and the resumed epilogue RETs to that. Here it landed after the +`setcontext` CALL, because *setcontext's* return address was sitting in +the slot: control appeared to flow straight on, with the counter still +at 1. + +**The same reasoning says why `swapcontext` got away with it**, and +that half is worth keeping so the lesson is not over-general: a context +saved by `swapcontext` is resumed while its stack is parked, so nothing +has run on it and the frame is intact. Sections 1 to 4 all passed. +*`getcontext` is the one that returns and is then resumed, and it is +the only one that could have shown this.* + +All three are assembly now, which is what `setjmp.s` was all along. +**The cost is one shared offset** -- `uc_sigmask` at 64 -- and +`makecontext.c` turns a change to it into a COMPILE error +(`size of array '_uc_layout_assert' is negative`) rather than a switch +that quietly restores the wrong mask. The mask is handled the way +`sigsetjmp` handles it, through `_psigblocked(SB)`, with `MOVL` +because `sigset_t` is `long` and kencc's `long` is four bytes. + +### And section 5 was the test's bug, of the one kind gcc cannot catch + +It read `volatile long a = 0x1111111111111111L;`. **kencc's `long` is +four bytes**, so `a` held `0x11111111` and the comparison against the +full 64-bit constant was false -- section 5 reported `got 0 want 1` for +a context switch that was working perfectly. `long long` now. + +*The glibc run could not have caught it*: this is the one class of test +bug the reference implementation is blind to, because the difference IS +the reference's width. The tree's first invariant, arriving inside a +file written to check something else. + +**What neither instrument covers**: nothing exercises a context switch with signals actually blocked. And the header could not be syntax-checked against the APE headers on the host -- they reach each other through absolute `/sys/include/ape/...` paths that resolve only diff --git a/sys/include/ape/ucontext.h b/sys/include/ape/ucontext.h index 9b6629c59..8e9a93bc1 100644 --- a/sys/include/ape/ucontext.h +++ b/sys/include/ape/ucontext.h @@ -37,17 +37,22 @@ typedef struct { } stack_t; /* - * `uc_mcontext' IS FIRST and must stay first: `_getmcontext' and - * friends are handed `&ucp->uc_mcontext' by the C wrappers, so nothing - * in the assembly depends on this layout -- but keeping the machine - * state at offset 0 means a debugger looking at a `ucontext_t' finds - * the registers where it expects them. + * ***THE FIRST TWO MEMBERS ARE A CONTRACT WITH `ucontext.s', WHICH + * ADDRESSES THEM BY NUMBER.*** `uc_mcontext' at 0 and `uc_sigmask' at + * 64 are what `getcontext', `setcontext' and `swapcontext' assume, and + * `arch/amd64/makecontext.c' turns a change here into a COMPILE error + * rather than a switch that quietly restores the wrong signal mask. + * The two after them are C's alone and may be reordered freely. + * + * *Those three are assembly, rather than C around a smaller primitive, + * because a C wrapper cannot save its CALLER's frame* -- `ucontext.s' + * has the failure that established it. */ typedef struct __ucontext { - mcontext_t uc_mcontext; + mcontext_t uc_mcontext; /* 0 */ + sigset_t uc_sigmask; /* 64 */ struct __ucontext *uc_link; stack_t uc_stack; - sigset_t uc_sigmask; } ucontext_t; extern int getcontext(ucontext_t *); diff --git a/sys/lib/tests/makectx-xcheck.c b/sys/lib/tests/makectx-xcheck.c index b35bf5e0f..25b099133 100644 --- a/sys/lib/tests/makectx-xcheck.c +++ b/sys/lib/tests/makectx-xcheck.c @@ -4,11 +4,11 @@ * sh makectx-xcheck.sh * * ***A CROSS-CHECK, AND IT COMPILES THE REAL FILE RATHER THAN A COPY - * OF IT.*** `lib/ap/arch/amd64/ucontext.c' is compiled straight into + * OF IT.*** `lib/ap/arch/amd64/makecontext.c' is compiled straight into * this program -- no extraction, no transcription, no markers to rot -- - * with stubs standing in for the three assembly primitives and for - * `sigprocmask'. That is possible because everything structural in - * that file is C; the assembly knows only "eight slots". + * with stubs standing in for `_ctxreturn' and `setcontext'. That is + * possible because the frame layout lives entirely in C; the assembly + * knows only the eight register slots and one asserted offset. * * WHY IT IS WORTH A FILE. The one number that has to agree across the * two languages is where the ucontext pointer sits in the new frame: @@ -32,6 +32,13 @@ * `base[UC_ARGMAX+1]' -> `base[UC_ARGMAX]' **3 failures** * `(...)base - 8' -> `(...)base' **1 failure** * + * and a third that is not a failure count at all: swapping + * `uc_sigmask' and `uc_link' in `' is a **compile error**, + * `size of array `_uc_layout_assert' is negative'. That is the one + * offset the assembly addresses by number, so it is asserted rather + * than trusted -- a wrong answer there would be a context switch + * quietly restoring the wrong signal mask. + * * Exit status is the failure count. */ @@ -43,13 +50,13 @@ #include /* the tree's, through the script's -I */ -/* ---- stubs for what lives in mcontext.s ---------------------- */ +/* ---- stubs for what lives in ucontext.s ---------------------- */ void _ctxreturn(void) { } /* * ***THE STUB MUST NOT RETURN, AND THE FIRST VERSION DID.*** The real - * `_setmcontext' jumps away and never comes back, so `_ctxfinish's + * `setcontext' jumps away and never comes back, so `_ctxfinish's * `exit(0)' is unreachable once a `uc_link' has been followed. A stub * that simply recorded and returned fell through to that `exit', and * section 3's "did not exit" check failed -- *the instrument reporting @@ -57,24 +64,15 @@ void _ctxreturn(void) { } * with*. It longjmps instead, which is what "does not return" means in * a host program. */ -static const mcontext_t *last_set; +static const ucontext_t *last_set; static jmp_buf away; -void -_setmcontext(const mcontext_t *mc) -{ - last_set = mc; - longjmp(away, 1); -} - -void _swapmcontext(mcontext_t *o, const mcontext_t *n) { (void)o; last_set = n; } -int _getmcontext(mcontext_t *mc) { (void)mc; return 0; } - int -sigprocmask(int how, sigset_t *set, sigset_t *oset) +setcontext(const ucontext_t *ucp) { - (void)how; (void)set; (void)oset; - return 0; + last_set = ucp; + longjmp(away, 1); + return -1; } /* `_ctxfinish' ends in exit(0) when uc_link is null; catch that. */ @@ -83,7 +81,7 @@ static int exited; static void xexit(int n) { (void)n; exited = 1; } /* ---- the real file ------------------------------------------- */ -#include "ucontext.c" +#include "makecontext.c" #undef exit @@ -164,22 +162,22 @@ main(void) * pointer, uc_link is garbage and the usual result is a silent * exit. */ - last_set = (const mcontext_t *)0; + last_set = (const ucontext_t *)0; exited = 0; if(setjmp(away) == 0) _ctxfinish((void *)(base + 1)); - eq("it followed uc_link", last_set == &link.uc_mcontext, 1); + eq("it followed uc_link", last_set == &link, 1); eq("and did not fall through to exit", exited, 0); printf("\nSection 4: a null uc_link exits instead\n"); uc.uc_link = (ucontext_t *)0; base[UC_ARGMAX+1] = (unsigned long long)(uintptr_t)&uc; - last_set = (const mcontext_t *)0; + last_set = (const ucontext_t *)0; exited = 0; if(setjmp(away) == 0) _ctxfinish((void *)(base + 1)); eq("it exited", exited, 1); - eq("and followed no link", last_set == (const mcontext_t *)0, 1); + eq("and followed no link", last_set == (const ucontext_t *)0, 1); printf("\nSection 5: every argument slot, at the cap\n"); memset(&uc, 0, sizeof uc); diff --git a/sys/lib/tests/makectx-xcheck.sh b/sys/lib/tests/makectx-xcheck.sh index 7b946d9dd..344b097a6 100644 --- a/sys/lib/tests/makectx-xcheck.sh +++ b/sys/lib/tests/makectx-xcheck.sh @@ -1,15 +1,15 @@ #!/bin/sh -# makectx-xcheck.sh -- compile libap's real `ucontext.c' on the build -# host, beside stubs for the three assembly primitives, and check that +# makectx-xcheck.sh -- compile libap's real `makecontext.c' on the build +# host, beside stubs for `_ctxreturn' and `setcontext', and check that # the frame `makecontext' builds is the one `_ctxfinish' reads back. # # sh makectx-xcheck.sh # # ***NOTHING IS EXTRACTED OR TRANSCRIBED HERE.*** The harness -# `#include's the library file itself, which is possible because -# everything structural in it is C -- the assembly knows only "eight -# slots". So there are no markers to rot, and the thing under test is -# the thing that ships. +# `#include's the library file itself, which is possible because the +# frame layout lives entirely in C -- the assembly knows only the eight +# register slots and one asserted offset. So there are no markers to +# rot, and the thing under test is the thing that ships. # # The three headers it needs are the tree's own: `' from # `sys/include/ape' and `' from `amd64/include/ape'. @@ -30,7 +30,7 @@ set -e # This file is three levels below the repository root. ROOT=$(cd "$(dirname "$0")/../../.." && pwd) -SRC=$ROOT/sys/src/ape/lib/ap/arch/amd64/ucontext.c +SRC=$ROOT/sys/src/ape/lib/ap/arch/amd64/makecontext.c HDR=$ROOT/sys/include/ape/ucontext.h ARCH=$ROOT/amd64/include/ape/ucontext_arch.h HERE=$(cd "$(dirname "$0")" && pwd) @@ -43,7 +43,7 @@ done rm -rf "$TMP"; mkdir -p "$TMP" ln -s "$HDR" "$TMP/ucontext.h" ln -s "$ARCH" "$TMP/ucontext_arch.h" -ln -s "$SRC" "$TMP/ucontext.c" +ln -s "$SRC" "$TMP/makecontext.c" : > "$TMP/lib.h" cat > "$TMP/signal.h" <<'EOF' #ifndef _MAKECTX_STUB_SIGNAL_H diff --git a/sys/lib/tests/ucontext-test.c b/sys/lib/tests/ucontext-test.c index 5ab9ea6de..9b01ceb98 100644 --- a/sys/lib/tests/ucontext-test.c +++ b/sys/lib/tests/ucontext-test.c @@ -130,14 +130,27 @@ s4_fn(void) static ucontext_t s5_main, s5_ctx; static int s5_seen; +/* + * ***`long long', NOT `long', AND THAT IS WHAT THE FIRST VM RUN + * FOUND.*** This said `volatile long a = 0x1111111111111111L;', which + * passes on glibc and cannot on this machine: **kencc's `long` is four + * bytes**, so `a' held `0x11111111' and the comparison against the full + * 64-bit constant was false. Section 5 reported `got 0 want 1' for a + * context switch that was working perfectly. + * + * *The gcc check could not have caught it* -- this is the one class of + * test bug that the reference implementation is blind to, because the + * difference IS the reference's width. The tree's own first invariant, + * arriving in the file written to check something else. + */ static void s5_fn(void) { - volatile long a = 0x1111111111111111L; - volatile long b = 0x2222222222222222L; + volatile long long a = 0x1111111111111111LL; + volatile long long b = 0x2222222222222222LL; swapcontext(&s5_ctx, &s5_main); - if(a == 0x1111111111111111L && b == 0x2222222222222222L) + if(a == 0x1111111111111111LL && b == 0x2222222222222222LL) s5_seen = 1; swapcontext(&s5_ctx, &s5_main); } diff --git a/sys/src/ape/lib/ap/arch/amd64/ucontext.c b/sys/src/ape/lib/ap/arch/amd64/makecontext.c similarity index 66% rename from sys/src/ape/lib/ap/arch/amd64/ucontext.c rename to sys/src/ape/lib/ap/arch/amd64/makecontext.c index b3db5388d..2a51d9f01 100644 --- a/sys/src/ape/lib/ap/arch/amd64/ucontext.c +++ b/sys/src/ape/lib/ap/arch/amd64/makecontext.c @@ -1,38 +1,38 @@ /* - * POSIX user contexts: the C half. + * POSIX user contexts: `makecontext' and the function a context + * returns through. `ucontext.s' has `getcontext', `setcontext' and + * `swapcontext'. * - * `ucontext.s' holds three register-only primitives that know nothing - * but "eight slots"; everything structural is here, so **no offset into - * `ucontext_t' is ever written down in two languages**. See that file's - * header for why the register set is eight and why the restore must not - * write to the stack. + * WHERE IT CAME FROM. php's `Zend/zend_fibers.c' is unconditional -- + * `Fiber' is a language class -- and wants either boost.context's GAS + * assembly or ; APExp had neither. `docs/notes/php.md'. * - * WHERE IT CAME FROM. php's `Zend/zend_fibers.c' is unconditional and - * wants either boost.context's GAS assembly or this; APExp had neither. - * `docs/notes/php.md' has the survey. - * - * ***THE WRAPPERS BELOW ARE ARCHITECTURE-INDEPENDENT AND LIVE IN AN - * ARCHITECTURE DIRECTORY ANYWAY***, because `makecontext' is NOT -- the - * argument layout is 6c's amd64 convention -- and because a shared - * `ucontext/' directory would be built for all eleven targets and link - * against `_getmcontext' on ten of them that do not have it. *A second - * architecture is the right moment to split this*, not before: there - * would be nothing to check the split against today. + * ***THIS FILE IS ARCHITECTURE-SPECIFIC AND THE REASON IS THE ARGUMENT + * LAYOUT***, which is 6c's amd64 convention and nothing else's. A + * second architecture adds its own pair beside this one. */ #include "lib.h" #include -#include #include +#include #include #include -#include #include -extern int _getmcontext(mcontext_t *); -extern void _setmcontext(const mcontext_t *); -extern void _swapmcontext(mcontext_t *, const mcontext_t *); extern void _ctxreturn(void); +/* + * ***THE ONE OFFSET `ucontext.s' KNOWS, ASSERTED RATHER THAN TRUSTED.*** + * The three switches are assembly because a C wrapper cannot save its + * caller's frame (see `ucontext.s'), and the price is that they address + * `uc_sigmask' by number. A negative array size turns a layout change + * into a COMPILE error instead of a context switch that quietly + * restores the wrong signal mask. + */ +typedef char _uc_layout_assert[ + (offsetof(ucontext_t, uc_mcontext) == 0 + && offsetof(ucontext_t, uc_sigmask) == 64) ? 1 : -1]; + /* * THE NEW CONTEXT'S STACK FRAME, as `makecontext' builds it and * `_ctxreturn' reads it back. Addresses increase downward in this @@ -47,8 +47,8 @@ extern void _ctxreturn(void); * `func' is entered with SP = base, exactly as a CALL would leave it. * When it returns, the RET pops base[0] and `_ctxreturn' sees * SP = &base[1], so the ucontext is at sp[UC_ARGMAX] -- **one constant, - * in one language**, which is the whole reason `_ctxreturn' hands its - * stack pointer to C instead of dereferencing it itself. + * in one language**, which is why `_ctxreturn' hands its stack pointer + * to C instead of dereferencing it itself. * * ***UC_ARGMAX IS A CAP AND NOT A CONVENTION, AND THE REASON IS 6c's * FIRST-ARGUMENT REGISTER.*** On an ABI that passes the first six @@ -83,51 +83,6 @@ ucdie(const char *msg) abort(); } -int -getcontext(ucontext_t *ucp) -{ - if(ucp == 0){ - errno = EINVAL; - return -1; - } - sigprocmask(SIG_SETMASK, (sigset_t*)0, &ucp->uc_sigmask); - /* - * Returns 0 now, and 0 again when a later _setmcontext resumes - * the saved PC -- which is this function's own return address, so - * control re-enters here and falls out of the bottom. The - * sigprocmask above is deliberately NOT re-run on that path: the - * side that resumes a context is the side that installs its mask. - */ - return _getmcontext(&ucp->uc_mcontext); -} - -int -setcontext(const ucontext_t *ucp) -{ - if(ucp == 0){ - errno = EINVAL; - return -1; - } - /* sigprocmask's `set' is not const (signal/sigprocmask.c:8) */ - sigprocmask(SIG_SETMASK, (sigset_t*)&ucp->uc_sigmask, (sigset_t*)0); - _setmcontext(&ucp->uc_mcontext); - errno = EINVAL; /* not reached */ - return -1; -} - -int -swapcontext(ucontext_t *oucp, const ucontext_t *ucp) -{ - if(oucp == 0 || ucp == 0){ - errno = EINVAL; - return -1; - } - sigprocmask(SIG_SETMASK, (sigset_t*)0, &oucp->uc_sigmask); - sigprocmask(SIG_SETMASK, (sigset_t*)&ucp->uc_sigmask, (sigset_t*)0); - _swapmcontext(&oucp->uc_mcontext, &ucp->uc_mcontext); - return 0; -} - void makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...) { @@ -183,7 +138,7 @@ makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...) ucp->uc_mcontext.mc_regs[_MC_ARG] = 0; /* - * `_setmcontext' loads SP and then adds 8 before jumping -- the + * `setcontext' loads SP and then adds 8 before jumping -- the * no-stack-write form of a RET -- so the SP recorded here is one * slot BELOW the frame, and `func' begins with SP = base. */ @@ -200,6 +155,7 @@ makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...) * for a new context is the CALLER's, which the required * `getcontext' has already put there. Zeroing them would hand the * new context a null view of every register-cached global. + * `uc_sigmask' is left alone for the same reason. */ } diff --git a/sys/src/ape/lib/ap/arch/amd64/mcontext.s b/sys/src/ape/lib/ap/arch/amd64/mcontext.s deleted file mode 100644 index db911b910..000000000 --- a/sys/src/ape/lib/ap/arch/amd64/mcontext.s +++ /dev/null @@ -1,128 +0,0 @@ -/* - * amd64 machine-context switch for . - * - * The file is `mcontext.s' and not `ucontext.s' because `ucontext.c' - * sits beside it and mk derives both objects from the base name. - * - * ***THIS IS `setjmp.s' WITH THE RETURN-VALUE CONVENTION CHANGED, AND - * THAT IS THE WHOLE DESIGN.*** The two problems are the same one: save - * enough of the machine that execution can be resumed somewhere else. - * Read `setjmp.s' first -- its header carries the two findings this - * file inherits rather than re-derives: - * - * - **eight registers, not two.** 6c has REGEXT = D_R15, so R15 and - * R14 are reserved program-wide and R13/R12 may follow them as - * caches for global variables. Restoring only SP and PC leaves the - * resumed code reading another context's globals. - * - **the restore must not WRITE to the stack.** Upstream longjmp - * stores the target PC at 0(SP) and RETs, which faults when SP is - * at USTKTOP. `ADDQ $8,SP' then `JMP' does the same thing without - * the store, and a ucontext resumed at the very top of a freshly - * allocated fiber stack is exactly that case. - * - * ***AND THE ONE THING THESE ROUTINES KNOW IS "EIGHT SLOTS".*** No - * offset of `uc_link', `uc_stack' or `uc_sigmask' appears here; - * `ucontext.c' passes `&ucp->uc_mcontext' and does the rest in C, so no - * structure offset is written down in two languages. `_ctxreturn' hands - * its own SP to C for the same reason -- *the frame layout belongs to - * `makecontext', which is C, so the stub carries no magic number.* - * - * Plan 9 amd64 calling convention (6c): RARG (= BP) carries the first - * argument, and the SECOND is at 8(FP), because regaalloc1() reserves a - * 0(FP) slot for the first even though it arrives in a register. - * - * ***AND NO REGISTER SURVIVES A CALL HERE, WHICH IS MEASURED.*** - * `out/bin/6c -S' on a function that computes three values, calls - * through a function pointer and then uses them spills all three to the - * stack and touches BX/R12-R15 not at all. So `_ctxreturn' could not - * have been written to keep the ucontext pointer in a callee-saved - * register across the context's own function -- *there are none* -- - * which is why `makecontext' puts it on the new stack instead. - * (`main9.s:7' calls R12-R15 "callee-saved REGEXT registers"; both can - * be true, since a register the allocator never hands out is trivially - * preserved. Nothing here relies on either reading.) - */ - -/* - * _getmcontext(mcontext_t *mc) -- save; returns 0 here, and 0 again - * when a later _setmcontext resumes it. - */ -TEXT _getmcontext(SB), $0 - MOVQ SP, 0(RARG) - MOVQ 0(SP), AX /* return PC */ - MOVQ AX, 8(RARG) - MOVQ BP, 16(RARG) /* BP = RARG = mc; restored on resume */ - MOVQ BX, 24(RARG) - MOVQ R12, 32(RARG) - MOVQ R13, 40(RARG) - MOVQ R14, 48(RARG) - MOVQ R15, 56(RARG) /* R15 = REGEXT */ - MOVL $0, AX - RET - -/* - * _setmcontext(const mcontext_t *mc) -- resume; does not return. - * - * AX is zeroed before the jump so that the `_getmcontext' or - * `_swapmcontext' being resumed appears to return 0, the same reason - * longjmp sets it. *Without that line a resumed getcontext returns - * whatever happened to be in AX*, and its caller believes it. - */ -TEXT _setmcontext(SB), $0 - MOVQ RARG, R11 /* R11 = mc (scratch, never in the set) */ - MOVQ 16(R11), BP - MOVQ 24(R11), BX - MOVQ 32(R11), R12 - MOVQ 40(R11), R13 - MOVQ 48(R11), R14 - MOVQ 56(R11), R15 - MOVQ 0(R11), SP /* restore SP */ - ADDQ $8, SP /* skip the return-address slot */ - MOVQ 8(R11), R11 /* R11 = target PC */ - MOVL $0, AX /* resumed call returns 0 */ - JMP R11 /* no stack write: safe at USTKTOP */ - -/* - * _swapmcontext(mcontext_t *old, const mcontext_t *new) - * - * The second argument is read FIRST, because everything after it - * clobbers the stack pointer it would have been read through. - */ -TEXT _swapmcontext(SB), $0 - MOVQ newmc+8(FP), R11 /* second arg, before SP moves */ - - MOVQ SP, 0(RARG) - MOVQ 0(SP), AX /* return PC */ - MOVQ AX, 8(RARG) - MOVQ BP, 16(RARG) - MOVQ BX, 24(RARG) - MOVQ R12, 32(RARG) - MOVQ R13, 40(RARG) - MOVQ R14, 48(RARG) - MOVQ R15, 56(RARG) - - MOVQ 16(R11), BP - MOVQ 24(R11), BX - MOVQ 32(R11), R12 - MOVQ 40(R11), R13 - MOVQ 48(R11), R14 - MOVQ 56(R11), R15 - MOVQ 0(R11), SP - ADDQ $8, SP - MOVQ 8(R11), R11 - MOVL $0, AX - JMP R11 - -/* - * _ctxreturn -- where a makecontext'd function RETURNS to. - * - * It is reached by RET, so there is no return address at 0(SP): the - * stack pointer is whatever `makecontext' arranged, one slot past the - * cell that held this address. That SP is the only thing needed to - * find the ucontext again, and `makecontext' -- which built the frame - * -- is the only code that should know where in it the pointer sits. - * So this hands SP to C and stops. - */ -TEXT _ctxreturn(SB), $0 - MOVQ SP, RARG /* _ctxfinish(sp) */ - JMPF _ctxfinish(SB) diff --git a/sys/src/ape/lib/ap/arch/amd64/mkfile b/sys/src/ape/lib/ap/arch/amd64/mkfile index ee71a9156..f6fe6ab17 100644 --- a/sys/src/ape/lib/ap/arch/amd64/mkfile +++ b/sys/src/ape/lib/ap/arch/amd64/mkfile @@ -11,8 +11,8 @@ OFILES=\ main9p.$O\ notetramp.$O\ setjmp.$O\ - mcontext.$O\ ucontext.$O\ + makecontext.$O\ strchr.$O\ strlen.$O\ tas.$O\ diff --git a/sys/src/ape/lib/ap/arch/amd64/ucontext.s b/sys/src/ape/lib/ap/arch/amd64/ucontext.s new file mode 100644 index 000000000..d5bfe42d1 --- /dev/null +++ b/sys/src/ape/lib/ap/arch/amd64/ucontext.s @@ -0,0 +1,145 @@ +/* + * amd64 context switch for : getcontext, setcontext, + * swapcontext. `makecontext.c' has the rest. + * + * ***THIS IS `setjmp.s' WITH THE RETURN-VALUE CONVENTION CHANGED, AND + * THAT IS THE WHOLE DESIGN.*** The two problems are the same one. Read + * that file first; both of its findings carry over unchanged: + * + * - **eight registers, not two.** 6c has REGEXT = D_R15, so R15 and + * R14 are reserved program-wide (`6c/reg.c:55-57') and R13/R12 may + * follow them as caches for global variables. Restoring only SP and + * PC leaves the resumed code reading another context's globals. + * - **the restore must not WRITE to the stack.** Upstream longjmp + * stores the target PC at 0(SP) and RETs, which faults when SP is + * at USTKTOP. `ADDQ $8,SP' then `JMP' does the same without the + * store, and a context resumed at the top of a freshly allocated + * fiber stack is exactly that case. + * + * ***AND ALL THREE ARE ASSEMBLY BECAUSE A C WRAPPER CANNOT SAVE ITS + * CALLER'S FRAME, WHICH COST A ROUND TO LEARN.*** The first version had + * these as C around three register-only primitives, so what + * `getcontext' saved was a point inside `getcontext' itself. That is + * fine until the context is RESUMED after `getcontext' has returned -- + * by then the wrapper's frame is dead stack, its own return-address + * slot has been reused by whatever the caller did next, and the resumed + * epilogue RETs to that. `ucontext-test' section 0 printed its PASS + * line once instead of twice and reported `setcontext came back round: + * got 1 want 2': control had landed after the `setcontext' CALL, + * because *setcontext's* return address was sitting in the slot. + * + * **The same reasoning says why `swapcontext' got away with it**, which + * is worth stating so the lesson is not over-general: a context saved + * by `swapcontext' is resumed while that stack is parked, so nothing + * has run on it and the frame is still intact. Sections 1 to 4 passed + * for that reason. *`getcontext' is the one that returns and then gets + * resumed*, and it is the only one that could have shown this. + * + * **The cost of asm is one shared offset**, `uc_sigmask' at 64, which + * `makecontext.c' asserts at COMPILE TIME rather than trusting. The + * mask itself is handled as `sigsetjmp' handles it -- through + * `_psigblocked(SB)' -- and with `MOVL', because `sigset_t' is `long' + * and kencc's `long' is four bytes. + * + * Plan 9 amd64 calling convention (6c): RARG (= BP) carries the first + * argument, and the SECOND is at 8(FP), because regaalloc1() reserves a + * 0(FP) slot for the first even though it arrives in a register. + * + * ***AND NO REGISTER SURVIVES A CALL HERE, WHICH IS MEASURED.*** + * A host `6c -S' on a function that computes three values, calls + * through a function pointer and then uses them spills all three to the + * stack and touches BX and R12-R15 not at all. That is why + * `makecontext' puts its bookkeeping pointer on the new stack rather + * than in a register. (`main9.s:7' calls R12-R15 "callee-saved REGEXT + * registers"; both can be true, since a register the allocator never + * hands out is trivially preserved. Nothing here relies on either.) + */ + +#define UC_MASK 64 /* offsetof(ucontext_t, uc_sigmask) */ + +TEXT getcontext(SB), $0 + MOVL _psigblocked(SB), AX /* the VALUE, not the address */ + MOVL AX, UC_MASK(RARG) + MOVQ SP, 0(RARG) + MOVQ 0(SP), AX /* return PC: the CALLER's */ + MOVQ AX, 8(RARG) + MOVQ BP, 16(RARG) /* BP = RARG = ucp; restored on resume */ + MOVQ BX, 24(RARG) + MOVQ R12, 32(RARG) + MOVQ R13, 40(RARG) + MOVQ R14, 48(RARG) + MOVQ R15, 56(RARG) /* R15 = REGEXT */ + MOVL $0, AX + RET + +/* + * setcontext does not return. AX is zeroed before the jump so that the + * `getcontext' or `swapcontext' being resumed appears to return 0 -- + * the same reason longjmp sets it. *Without that line a resumed + * getcontext returns whatever was in AX and its caller believes it.* + */ +TEXT setcontext(SB), $0 + MOVQ RARG, R11 /* R11: scratch, never in the saved set */ + MOVL UC_MASK(R11), AX + MOVL AX, _psigblocked(SB) + MOVQ 16(R11), BP + MOVQ 24(R11), BX + MOVQ 32(R11), R12 + MOVQ 40(R11), R13 + MOVQ 48(R11), R14 + MOVQ 56(R11), R15 + MOVQ 0(R11), SP + ADDQ $8, SP /* skip the return-address slot */ + MOVQ 8(R11), R11 + MOVL $0, AX + JMP R11 /* no stack write: safe at USTKTOP */ + +/* + * swapcontext(ucontext_t *oucp, const ucontext_t *ucp) + * + * The second argument is read FIRST, because everything after it + * clobbers the stack pointer it would have been read through. + */ +TEXT swapcontext(SB), $0 + MOVQ ucp+8(FP), R11 /* second arg, before SP moves */ + + MOVL _psigblocked(SB), AX + MOVL AX, UC_MASK(RARG) + MOVQ SP, 0(RARG) + MOVQ 0(SP), AX /* return PC */ + MOVQ AX, 8(RARG) + MOVQ BP, 16(RARG) + MOVQ BX, 24(RARG) + MOVQ R12, 32(RARG) + MOVQ R13, 40(RARG) + MOVQ R14, 48(RARG) + MOVQ R15, 56(RARG) + + MOVL UC_MASK(R11), AX + MOVL AX, _psigblocked(SB) + MOVQ 16(R11), BP + MOVQ 24(R11), BX + MOVQ 32(R11), R12 + MOVQ 40(R11), R13 + MOVQ 48(R11), R14 + MOVQ 56(R11), R15 + MOVQ 0(R11), SP + ADDQ $8, SP + MOVQ 8(R11), R11 + MOVL $0, AX + JMP R11 + +/* + * _ctxreturn -- where a makecontext'd function RETURNS to. + * + * It is reached by RET, so there is no return address at 0(SP): the + * stack pointer is whatever `makecontext' arranged, one slot past the + * cell that held this address. That SP is the only thing needed to + * find the ucontext again, and `makecontext' -- which built the frame + * -- is the only code that should know where in it the pointer sits. + * So this hands SP to C and stops, and no frame offset is written down + * in two languages. + */ +TEXT _ctxreturn(SB), $0 + MOVQ SP, RARG /* _ctxfinish(sp) */ + JMPF _ctxfinish(SB)