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)