diff --git a/.gitignore b/.gitignore index bb99cb0741..4e2eb4e996 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,7 @@ sys/src/ape/cmd/clisp/build-*/ # munmaptrim-xcheck's generated extract and its host binary sys/lib/tests/munmaptrim-extract.h sys/lib/tests/munmaptrim-xcheck + +# makectx-xcheck's host binary and its scratch include directory +sys/lib/tests/makectx-xcheck +sys/lib/tests/.makectx-xcheck.d/ diff --git a/CLAUDE.md b/CLAUDE.md index d93449a90e..e0b56a0645 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -271,6 +271,14 @@ reach its start routine and `pthread_cond_timedwait` must actually wait; the two are different bugs and the sections are split so), `poll-test` (the count is of pollfd ENTRIES, a negative fd is ignored, a closed one is POLLNVAL — the two rules whose absence makes a `while(poll())` caller SPIN), +`ucontext-test` (**the one test here with a REAL REFERENCE +IMPLEMENTATION** — glibc has ucontext, so all six sections were checked +against a working one before the VM saw them, 0 failures; *a failure on +Plan 9 that also fails on gcc is the test's bug*. Section 1 asks whether +the function ran **on the stack it was given**, which a switch that +quietly used the caller's would otherwise pass; section 3 passes the +maximum arguments because the ucontext cell sits just above them and the +failure mode of an off-by-one is `exit(0)`, so it prints a marker first), `rdev-test` (two different files must be distinguishable — it transcribes diffutils' `same_file()` verbatim, because the question is what THAT function answers), `stdio-test`. @@ -282,8 +290,8 @@ so a test built from a fresh pull can run days-old library code and say nothing. `_sock_listenmark`, `_execmark`, `_ttymark`, `_getcwdmark`, `_printfmark`, `_dupmark`, `_dotdirmark`, `_tempmark`, `_fdinfomark`, `_umaskmark`, `_bufmark`, `_rdevmark`, `_selregmark`, `_pthreadmark`, -`_syncmark`, `_mmapmark` each make the test **fail to LINK** against a libap -predating its fix. The idiom has paid twelve times; bump one when a file changes in a way a test must see — and +`_syncmark`, `_mmapmark`, `_ucmark` each make the test **fail to LINK** against a libap +predating its fix. The idiom has paid thirteen times; bump one when a file changes in a way a test must see — and it matters most where the bug's whole symptom is that nothing is printed. @@ -313,6 +321,18 @@ field was. The harness wraps `malloc` now, as it already wrapped `free`. *The assertion had been phrased against the thing under test instead of against something independent of it*, which is the shape to look for when a break does not fire. +**`makectx-xcheck.{c,sh}`** checks `makecontext`'s FRAME on the host, +and **it compiles the real `ucontext.c` rather than a copy** — no +extraction and no markers to rot, which is possible only because +everything structural in that file is C and the assembly knows just +"eight slots". It exists because the one number spanning the two +languages (where the ucontext pointer sits in the new frame) fails +SILENTLY on the VM. No external oracle, so its control is a deliberate +break: the cell off by one gives **3** failures, `SP` not one slot below +**1**. ***Its first run reported its own STUB***: the real +`_setmcontext` never returns, the fake recorded and returned, and +section 3 duly failed — *the assertion was right and the fake was +wrong*, which is the shape to recognise rather than argue with. **`hostkencc/clisp-mirror.sh` stages clisp's generated tree on the host** so the host kencc can compile and inspect it -- `--flags` prints the compile line, `--compile` builds all 31 TUs and GATES. *Its @@ -485,8 +505,16 @@ and 3 say both trims survive. **But three of the eight `mmap` calls FAILED**, and 10MB is not a memory limit, so **section 0b** asks how many mappings a process may HOLD, at three sizes, *with the prediction written down first*: slots give the same count at every size, address -space gives one falling as 1/size. It is **5 at 2MB and 5 at 16MB**, -and 64 at a size below `SEG_THRESHOLD` — the control. **Two of its +space gives one falling as 1/size. It was **5 at 2MB and 5 at 16MB**, +and 64 at a size below `SEG_THRESHOLD` — the control. ***AND THE FIX +DISARMED IT***: with `SEG_THRESHOLD` at 64MB all three of 0b's sizes +take the malloc arm, so it answers 64/64/64 — right, worth keeping as +the regression check that `mmap` spends no slot, and *unable to say +how many slots exist*. **Section 0c** asks the platform instead, +`_SEGATTACH` in a loop at 64KB: **5**, the same number over a 256-fold +range of sizes for a total of 320KB, which excludes memory outright. +*A section whose cases have collapsed onto one path is a check that +can only pass.* **Two of its sections can kill the process, so it takes an ARGUMENT**: no argument runs the tail trim, `3` runs the head trim, never sharing a run, because a section that can kill the process hides every section after @@ -1121,11 +1149,12 @@ two are the opposite of what it says — `HAVE_TM_ZONE` and `HAVE_ST_BLOCKS` are both YES, `struct tm` and `struct stat` having those members. *A sweep over function names, asked about a struct field, answers `absent` confidently.* -**What can still stop it, in order**: (1) **fibers have no context -switch** — `zend_fibers.c` is unconditional and wants either -boost.context's GAS assembly or ``, and APExp has neither; -a libap `ucontext` for amd64 is the reusable half, and it is a decision -to make BEFORE the mkfile, since it changes what the mkfile builds. +**What can still stop it, in order**: (1) fibers — **CLOSED on the +host, unmeasured on the VM**: `zend_fibers.c` is unconditional and +wants either boost.context's GAS assembly or ``, and APExp +now has the second (`lib/ap/arch/amd64/mcontext.s` + `ucontext.c`, +`docs/notes/libap.md`). amd64 only; the other ten follow the same +shape when wanted. (2) **`ext/opcache` is mandatory in 8.5** — there is no `--disable-opcache`, only `--disable-opcache-jit`, which is what drops the 13-file `jit/ir` code generator; opcache only has to COMPILE, since @@ -1690,12 +1719,25 @@ the index, so that nothing here is a surprise. reason it scrubs `FD_ISTTY` and `FD_ISREG`. `FD_BUFFEREDX` is poison: `read()` and `select()` both answer EIO for it without touching the fd. -- **A process gets FIVE spare segment slots, measured**, so `mmap` - cannot be backed by `segattach` in general: `mmaptrim-probe` section - 0b maps until refused at three sizes and answers 5 at 2MB and 5 at - 16MB -- *the same count for eight times the memory*. `SEG_THRESHOLD` - is 64MB for that reason and anything below it is `malloc`-backed, - page-aligned by hand since `malloc` is not. +- **kencc amd64 has NO callee-saved registers, 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. *So nothing may be kept in a + register across a call into arbitrary code* — which is why libap's + `ucontext` puts its bookkeeping pointer on the new stack. `main9.s:7` + calls R12-R15 "callee-saved REGEXT registers" and both can be true: a + register the allocator never hands out is trivially preserved. +- **A process gets FIVE spare segment slots, measured twice and the + second time at 64KB**, so `mmap` cannot be backed by `segattach` in + general. `mmaptrim-probe` section 0b got 5 at 2MB and 5 at 16MB + through `mmap`; **section 0c calls `_SEGATTACH` directly and gets 5 + at 64KB**. *Five, over a 256-fold range of sizes and a total of + 320KB* -- so memory and address space are excluded outright, and the + number is the kernel's rather than the library's. `SEG_THRESHOLD` is + 64MB for that reason and anything below it is `malloc`-backed, + page-aligned by hand since `malloc` is not. **0c is the one that + still asks**: every size 0b uses is now under the threshold, so it + answers 64/64/64 and can only confirm that `mmap` spends no slot. - `/dev/snarf` is the clipboard and has no concept of ownership. - Plan 9 has no loopback unless `ip/ipconfig loopback /dev/null 127.1` has been run -- and that belongs in the machine's startup, not in diff --git a/amd64/include/ape/ucontext_arch.h b/amd64/include/ape/ucontext_arch.h new file mode 100644 index 0000000000..ba28c10cc5 --- /dev/null +++ b/amd64/include/ape/ucontext_arch.h @@ -0,0 +1,32 @@ +#ifndef __UCONTEXT_ARCH_H +#define __UCONTEXT_ARCH_H + +/* + * amd64 machine context for . + * + * ***THE EIGHT SLOTS ARE setjmp's EIGHT, AND THAT IS DELIBERATE.*** + * `arch/amd64/setjmp.s' saves SP, PC, BP, BX, R12, R13, R14, R15 and its + * header says why the last six are there: APExp's 6c has + * REGEXT = D_R15, so R15 and R14 are reserved program-wide + * (`6c/reg.c:55-57') and R13/R12 can follow them as external register + * caches for global variables. A context switch that restored only + * SP and PC would leave the resumed code reading another context's + * cached globals. *ucontext has exactly setjmp's problem, so it gets + * exactly setjmp's answer* -- and `arch/amd64/ucontext.s' is that file's + * save/restore sequence with the return-value convention changed. + * + * The order is fixed because the assembly indexes it by number. It is + * the ONLY thing the assembly knows about any of these structures: + * everything else -- `uc_link', `uc_stack', `uc_sigmask' -- is handled + * in C, so no struct offset is ever written down twice. + */ +typedef struct mcontext_t { + unsigned long long mc_regs[8]; +} mcontext_t; + +/* names for the three slots `makecontext' has to fill */ +#define _MC_SP 0 +#define _MC_PC 1 +#define _MC_ARG 2 /* BP, which is 6c's RARG: the first argument */ + +#endif /* __UCONTEXT_ARCH_H */ diff --git a/docs/notes/libap.md b/docs/notes/libap.md index 9f8a0668ba..444b590601 100644 --- a/docs/notes/libap.md +++ b/docs/notes/libap.md @@ -3900,6 +3900,23 @@ anything a program is at all likely to allocate. *The limit was known and the constant was chosen as though it were not.* At 1MB, php's Zend allocator -- 2MB chunks -- would have stopped at about 10MB of heap. +***AND THE NUMBER IS CONFIRMED INDEPENDENTLY, AT 64KB.*** Section 0c +calls `_SEGATTACH` in a loop rather than going through `mmap`, and +gets **5** -- the same five, now over a 256-fold range of sizes and a +total of 320KB, which leaves no reading in which memory or address +space is the limit. It also says the ceiling did not move when +`SEG_THRESHOLD` did, which is the point of asking the kernel rather +than the library. + +*That section exists because the fix disarmed the one that found the +problem.* With the threshold at 64MB all three of 0b's sizes take the +malloc arm, so it answers 64/64/64 -- correct, and worth keeping as +the regression check that `mmap` spends no slot, but **its smallest +size has stopped being a control and the run says nothing about how +many slots exist**. Read on its own it is indistinguishable from the +Linux control. A section whose cases have collapsed onto one path is +a check that can only pass. + 64MB now: a program needs five simultaneous 64MB mappings to reach the ceiling, and the detachable path is kept for the only case worth a slot, a mapping big enough that leaving it in the data segment for the @@ -3928,3 +3945,122 @@ already wrapped `free`, and the same break gives 3 failures. *A check that cannot fail is not a check*, and the way it got written is worth naming: the assertion was phrased against the thing under test instead of against something independent of it. + +## ucontext for amd64 + +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. This is the second, chosen because +it is libap's own code and reusable beyond php; the other ten +architectures can follow the same shape. + +**Three files.** `arch/amd64/mcontext.s` holds `_getmcontext`, +`_setmcontext`, `_swapmcontext` and the `_ctxreturn` stub; +`arch/amd64/ucontext.c` holds the four POSIX entry points and +`_ctxfinish`; `sys/include/ape/ucontext.h` and +`amd64/include/ape/ucontext_arch.h` are the interface. + +**It is `setjmp.s` with the return-value convention changed**, and +deliberately so: the two problems are the same one. Both of that file's +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 can follow + them as caches for global variables. A switch restoring only SP and + PC leaves the resumed code reading another context's globals. +* **the restore must not WRITE to the stack.** `ADDQ $8,SP` then `JMP` + instead of storing the PC at `0(SP)` and `RET`ing. A context resumed + at the top of a fresh fiber stack is exactly the case that faults. + +**And AX is zeroed before the jump.** Without it a `getcontext` resumed +by `setcontext` returns whatever was in AX and its caller believes the +number -- the same reason `longjmp` sets it, found by writing the +section that checks it rather than by reasoning. + +### No register survives a call here, and that is measured + +The first design kept the ucontext pointer in a callee-saved register +across the context's own function. **There are no callee-saved +registers.** `hostkencc/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 and R12-R15 not at all. So the +pointer goes on the new stack instead. + +*`main9.s:7` calls R12-R15 "callee-saved REGEXT registers", and both +statements can be true* -- a register the allocator never hands out is +trivially preserved -- but nothing in this implementation now depends on +either reading, which is the point. + +### The one number that spans two languages + +`makecontext` writes the ucontext pointer at `base[UC_ARGMAX+1]`; +`_ctxfinish` reads it at `sp[UC_ARGMAX]`, where `sp` is what +`_ctxreturn` hands over after the RET has popped one slot. **Off by one +and the pointer read back is an argument**, so `uc_link` is garbage and +the usual consequence is `exit(0)` -- indistinguishable from a clean +finish at a shell prompt. + +`_ctxreturn` therefore passes its stack pointer to C and does nothing +else: *the frame layout belongs to `makecontext`, which is C, so the +stub carries no magic number.* No offset into `ucontext_t` appears in +assembly at all. + +**`UC_ARGMAX` is a cap and not a convention.** On an ABI that passes the +first six arguments in registers the stack image does not change shape +with `argc`, so the bookkeeping cell can sit at a fixed offset whatever +the count -- how glibc does it. Here only argument zero is in a register +(6c's `RARG` = BP) and the rest are contiguous from `8(FP)`, so the cell +must follow a FIXED number of argument slots. Eight, chosen rather than +derived; php passes none. + +**`mc_regs[3..7]` are left exactly as they are, and that is why POSIX +makes `getcontext` a precondition of `makecontext`**: those registers +may hold 6c's caches for global variables, so the right value for a new +context is the caller's, which the required `getcontext` already put +there. Zeroing them would hand the new context a null view of every +register-cached global. + +**`stack_t` is defined in `` and not in ``.** +POSIX puts it beside `sigaltstack`, and APExp has no `sigaltstack`: a +probe compiling `stack_t ss;` against `` would answer yes and +the next thing such a package does is call the function. *This tree's +commonest bug shape with the sign flipped* -- not a stub answering +success but a TYPE answering for a capability that is not there. The +`external/` grep found 39 files; every C use is a consumer behind +`HAVE_SIGALTSTACK`, and the two `config.h` hits are gnulib's +replacement slot, which a plain typedef does not disturb. + +### Two instruments, and what each can and cannot say + +`sys/lib/tests/ucontext-test.c` is the switch, and **it has a real +reference implementation**, which is rare here: glibc has ucontext, so +all six sections were checked against a working one before the VM saw +them. 0 failures on gcc. A failure on Plan 9 that also fails on glibc is +the test's bug. + +`sys/lib/tests/makectx-xcheck.{c,sh}` is the frame arithmetic, on the +host. **It compiles the real `ucontext.c` rather than a copy** -- no +extraction, no markers to rot -- with stubs for the three assembly +primitives, which is possible only because everything structural in that +file is C. It exists because the off-by-one above fails *silently* on +the VM and costs a rebuild to find. No external oracle, so its control +is a deliberate break, run and reverted: the ucp cell off by one gives +**3** failures, `SP` not one slot below gives **1**. + +***AND ITS FIRST RUN REPORTED ITS OWN STUB.*** Section 3 failed on "did +not exit", because the real `_setmcontext` jumps away and never returns +while the stub recorded and returned, falling through to `_ctxfinish`'s +`exit(0)`. The stub `longjmp`s now, which is what "does not return" +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 +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 +inside `apexp-sh` -- so the gcc check symlinks the two ucontext headers +into a scratch directory instead and supplies `sigset_t` from glibc's +own ``. diff --git a/docs/notes/php.md b/docs/notes/php.md index 03a9e94df4..db3cfff446 100644 --- a/docs/notes/php.md +++ b/docs/notes/php.md @@ -239,7 +239,9 @@ version, then add one library at a time. ## Open, as a list -* fibers: no context switch (§1) — **decide `ucontext` or boost asm** +* fibers: `ucontext` is written (`docs/notes/libap.md`) and passes on + the host against glibc; **not yet run on the VM**, and php has not + been pointed at it (`ZEND_FIBER_UCONTEXT` in the `config.h`) * `main/php_config.h` not written yet; the inventory is derived, the answers are not * `memfd_create` is declared with no implementation, which is a libap diff --git a/sys/include/ape/ucontext.h b/sys/include/ape/ucontext.h new file mode 100644 index 0000000000..9b6629c59a --- /dev/null +++ b/sys/include/ape/ucontext.h @@ -0,0 +1,62 @@ +#ifndef __UCONTEXT_H +#define __UCONTEXT_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * POSIX user contexts. + * + * WHERE IT CAME FROM. php's `Zend/zend_fibers.c' is unconditional -- + * `Fiber' is a language class -- and needs one of two context switches: + * boost.context's GAS assembly, or this. APExp had neither. See + * `docs/notes/php.md'. + * + * ***`stack_t' IS DEFINED HERE AND NOT IN , ON PURPOSE.*** + * POSIX puts it in , beside `sigaltstack' -- and **APExp has + * no `sigaltstack'**. Putting the type where POSIX puts it would make + * an autoconf probe that compiles `stack_t ss;' against + * answer YES, and the next thing such a package does is call + * `sigaltstack'. *That is this tree's commonest bug shape with the + * sign flipped*: not a stub answering success, but a TYPE answering for + * a capability that is not there. Here the type is reachable only by + * including , which is a deliberate act. + * The `external/' grep before adding the name found 39 files; every + * C use is a CONSUMER behind `HAVE_SIGALTSTACK', and the two `config.h' + * hits are gnulib's `/`*` #undef stack_t *`/' replacement slot, which a + * plain typedef does not disturb. + */ +typedef struct { + void *ss_sp; + long ss_size; + int ss_flags; +} 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. + */ +typedef struct __ucontext { + mcontext_t uc_mcontext; + struct __ucontext *uc_link; + stack_t uc_stack; + sigset_t uc_sigmask; +} ucontext_t; + +extern int getcontext(ucontext_t *); +extern int setcontext(const ucontext_t *); +extern int swapcontext(ucontext_t *, const ucontext_t *); +extern void makecontext(ucontext_t *, void (*)(void), int, ...); + +#ifdef __cplusplus +} +#endif + +#endif /* __UCONTEXT_H */ diff --git a/sys/lib/tests/makectx-xcheck.c b/sys/lib/tests/makectx-xcheck.c new file mode 100644 index 0000000000..b35bf5e0fd --- /dev/null +++ b/sys/lib/tests/makectx-xcheck.c @@ -0,0 +1,202 @@ +/* makectx-xcheck.c -- does `makecontext' build the frame that + * `_ctxreturn' and `_ctxfinish' expect? Checked on the build host. + * + * 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 + * 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". + * + * 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: + * `makecontext' writes it at `base[UC_ARGMAX+1]' and `_ctxfinish' reads + * it at `sp[UC_ARGMAX]', where `sp' is what `_ctxreturn' hands over + * after the RET has popped one slot. **Off by one and the pointer read + * back is an argument**, so `uc_link' is garbage -- and the usual + * consequence is `exit(0)', which from a shell prompt is + * indistinguishable from a clean finish. *A silent wrong answer is + * exactly what a host check is for*, and on the VM the same mistake + * costs a full rebuild to find. + * + * It does NOT test the context switch: that is machine code for a + * machine this is not. `ucontext-test.c' is the instrument for that, + * and it has glibc as a reference implementation. + * + * THE CONTROL IS A DELIBERATE BREAK, run and reverted, because there is + * no external oracle for a layout this file and `ucontext.c' define + * between them: + * + * `base[UC_ARGMAX+1]' -> `base[UC_ARGMAX]' **3 failures** + * `(...)base - 8' -> `(...)base' **1 failure** + * + * Exit status is the failure count. + */ + +#include +#include +#include +#include +#include + +#include /* the tree's, through the script's -I */ + +/* ---- stubs for what lives in mcontext.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 + * `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 + * its own stub, which is the shape to recognise rather than to argue + * with*. It longjmps instead, which is what "does not return" means in + * a host program. + */ +static const mcontext_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) +{ + (void)how; (void)set; (void)oset; + return 0; +} + +/* `_ctxfinish' ends in exit(0) when uc_link is null; catch that. */ +static int exited; +#define exit xexit +static void xexit(int n) { (void)n; exited = 1; } + +/* ---- the real file ------------------------------------------- */ +#include "ucontext.c" + +#undef exit + +/* ---- the harness --------------------------------------------- */ + +static int fails; + +static void +eq(const char *what, long long got, long long want) +{ + if(got == want) + printf(" PASS %-44s %lld\n", what, got); + else { + printf(" FAIL %-44s got %lld want %lld\n", what, got, want); + fails++; + } +} + +#define STKSZ 65536 +static char stack[STKSZ]; + +static void dummy(void) { } + +int +main(void) +{ + ucontext_t uc, link; + unsigned long long *base, top; + int i; + + printf("makectx-xcheck: makecontext's frame, host build\n\n"); + + printf("Section 0: the three registers makecontext sets\n"); + memset(&uc, 0, sizeof uc); + uc.uc_stack.ss_sp = stack; + uc.uc_stack.ss_size = STKSZ; + uc.uc_link = &link; + for(i = 3; i < 8; i++) + uc.uc_mcontext.mc_regs[i] = 0xAAAA0000ULL + i; + + makecontext(&uc, dummy, 1, 0x1234); + + top = ((unsigned long long)(uintptr_t)stack + STKSZ) & ~15ULL; + base = (unsigned long long *)(uintptr_t)(top - (UC_ARGMAX+2)*8); + + eq("SP is one slot below the frame", + (long long)(uc.uc_mcontext.mc_regs[_MC_SP] + - (unsigned long long)(uintptr_t)base), -8); + eq("PC is the function", + uc.uc_mcontext.mc_regs[_MC_PC] == (unsigned long long)(uintptr_t)dummy, 1); + eq("the first argument is in the register slot", + (long long)uc.uc_mcontext.mc_regs[_MC_ARG], 0x1234); + + printf("\nSection 1: mc_regs[3..7] are LEFT ALONE\n"); + /* + * Not housekeeping: they may hold 6c's caches for global + * variables, so the right value for a new context is the one the + * required `getcontext' already put there. Zeroing them is the + * plausible wrong thing, which is why it is asserted. + */ + for(i = 3; i < 8; i++){ + char nm[48]; + sprintf(nm, "mc_regs[%d] untouched", i); + eq(nm, (long long)uc.uc_mcontext.mc_regs[i], 0xAAAA0000LL + i); + } + + printf("\nSection 2: the frame as _ctxreturn will find it\n"); + eq("base[0] is the return stub", + base[0] == (unsigned long long)(uintptr_t)_ctxreturn, 1); + eq("base[UC_ARGMAX+1] is the ucontext", + base[UC_ARGMAX+1] == (unsigned long long)(uintptr_t)&uc, 1); + + printf("\nSection 3: _ctxfinish finds it from the SP it is given\n"); + /* + * ***THE ONE NUMBER THAT SPANS THE TWO LANGUAGES.*** After the + * RET the stack pointer is &base[1], which is what `_ctxreturn' + * hands over. If this disagrees with where makecontext wrote the + * pointer, uc_link is garbage and the usual result is a silent + * exit. + */ + last_set = (const mcontext_t *)0; + exited = 0; + if(setjmp(away) == 0) + _ctxfinish((void *)(base + 1)); + eq("it followed uc_link", last_set == &link.uc_mcontext, 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; + 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); + + printf("\nSection 5: every argument slot, at the cap\n"); + memset(&uc, 0, sizeof uc); + uc.uc_stack.ss_sp = stack; + uc.uc_stack.ss_size = STKSZ; + uc.uc_link = &link; + makecontext(&uc, dummy, UC_ARGMAX, 101, 102, 103, 104, 105, 106, 107, 108); + eq("argument 0 in the register slot", + (long long)uc.uc_mcontext.mc_regs[_MC_ARG], 101); + for(i = 1; i < UC_ARGMAX; i++){ + char nm[48]; + sprintf(nm, "argument %d at base[%d]", i, i+1); + eq(nm, (long long)base[i+1], 101 + i); + } + eq("the ucontext cell SURVIVED the last argument", + base[UC_ARGMAX+1] == (unsigned long long)(uintptr_t)&uc, 1); + + printf("\nmakectx-xcheck: %d failures\n", fails); + return fails; +} diff --git a/sys/lib/tests/makectx-xcheck.sh b/sys/lib/tests/makectx-xcheck.sh new file mode 100644 index 0000000000..7b946d9dd1 --- /dev/null +++ b/sys/lib/tests/makectx-xcheck.sh @@ -0,0 +1,60 @@ +#!/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 +# 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. +# +# The three headers it needs are the tree's own: `' from +# `sys/include/ape' and `' from `amd64/include/ape'. +# **They cannot simply be put on the include path**, because the APE +# headers reach each other through ABSOLUTE `/sys/include/ape/...' +# paths that resolve only inside `apexp-sh's namespace -- a first +# attempt died on `stddef_arch.h:16'. So the two are SYMLINKED into a +# scratch directory that is searched first, beside a `' that +# supplies `sigset_t' from glibc's own `'. +# *That detail is not cosmetic*: defining `sigset_t' independently made +# `' conflict with it two includes later. +# +# It does NOT test the context switch -- that is machine code for a +# machine this is not. `ucontext-test.c' does, with glibc as a +# reference implementation. + +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 +HDR=$ROOT/sys/include/ape/ucontext.h +ARCH=$ROOT/amd64/include/ape/ucontext_arch.h +HERE=$(cd "$(dirname "$0")" && pwd) +TMP=$HERE/.makectx-xcheck.d + +for f in "$SRC" "$HDR" "$ARCH"; do + [ -f "$f" ] || { echo "makectx-xcheck: no $f -- is ROOT wrong?" >&2; exit 2; } +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" +: > "$TMP/lib.h" +cat > "$TMP/signal.h" <<'EOF' +#ifndef _MAKECTX_STUB_SIGNAL_H +#define _MAKECTX_STUB_SIGNAL_H +#include /* glibc's own, so agrees */ +#define SIG_SETMASK 2 +extern int sigprocmask(int, sigset_t *, sigset_t *); +#endif +EOF + +cc=${CC:-gcc} +$cc -Wall -Wextra -Wno-unused-parameter -I"$TMP" \ + -o "$HERE/makectx-xcheck" "$HERE/makectx-xcheck.c" +"$HERE/makectx-xcheck" diff --git a/sys/lib/tests/mmaptrim-probe.c b/sys/lib/tests/mmaptrim-probe.c index 23a05c096d..7966339b19 100644 --- a/sys/lib/tests/mmaptrim-probe.c +++ b/sys/lib/tests/mmaptrim-probe.c @@ -265,6 +265,12 @@ main(int argc, char **argv) * reaches past the POSIX surface on purpose: the number it reports * is a property of the kernel, and going through mmap to get it * would measure the library's policy instead. + * + * **MEASURED: 5.** The same five 0b got at 2MB and at 16MB, now at + * 64KB -- *a 256-fold range of sizes, and 320KB in total* -- which + * leaves no reading in which memory or address space is the limit. + * It also says the number did not move when SEG_THRESHOLD did, + * which is the point of asking the kernel rather than the library. */ #ifndef __linux__ { diff --git a/sys/lib/tests/ucontext-test.c b/sys/lib/tests/ucontext-test.c new file mode 100644 index 0000000000..5ab9ea6de7 --- /dev/null +++ b/sys/lib/tests/ucontext-test.c @@ -0,0 +1,250 @@ +/* ucontext-test.c -- getcontext/makecontext/swapcontext/setcontext + * + * pcc -o ucontext-test ucontext-test.c && ./ucontext-test + * gcc -o ucontext-test ucontext-test.c && ./ucontext-test # the control + * + * ***THIS ONE HAS A REAL REFERENCE IMPLEMENTATION, WHICH IS RARE HERE.*** + * glibc has ucontext, so every assertion below can be checked against a + * working implementation before the VM ever sees it -- the tree's own + * rule about checking a new test on gcc first, for once with the unit + * under test present on both sides rather than only the harness. **A + * failure on Plan 9 that also fails on glibc is this file's bug.** + * + * WHERE IT CAME FROM. php's `Zend/zend_fibers.c' is unconditional -- + * `Fiber' is a language class -- and needs either boost.context's GAS + * assembly or `'. APExp had neither, so the second was + * written: `lib/ap/arch/amd64/mcontext.s' (three register-only + * primitives) and `ucontext.c' (everything structural). + * `docs/notes/php.md' has the survey it came out of. + * + * WHAT EACH SECTION IS FOR, because several of them look alike and are + * not: + * + * 0 a context resumed by `setcontext' RETURNS 0 from its + * `getcontext'. The assembly zeroes AX before jumping for this + * reason alone, exactly as longjmp does -- *without it a resumed + * getcontext returns whatever was in AX and its caller believes + * the number*. + * 1 a function runs on the new stack AND the stack is really the one + * that was handed over -- checked by taking the address of a local + * and asking whether it lies inside the allocation. A context + * switch that quietly ran on the caller's stack would pass every + * other section here. + * 2 `uc_link' is honoured when the function RETURNS. + * 3 ***THE ARGUMENTS, AND THIS IS THE ONE THAT CHECKS THE FRAME + * LAYOUT.*** It passes the maximum this port accepts, because the + * cell holding the ucontext sits immediately above the argument + * slots: an off-by-one puts the last argument on top of it, and + * then `_ctxfinish' reads a bad pointer. **The dangerous outcome + * is silence** -- a null-ish `uc_link' means `exit(0)', which from + * the shell is indistinguishable from a clean finish -- so the + * section prints a marker before the call and the count after it. + * 4 ping-pong, which is what a fiber actually does: many switches + * through one pair of contexts, each resuming where it left off. + * 5 a context's own locals survive being switched away from and back. + * + * Exit status is the failure count. + */ + +#include +#include +#include +#include + +#ifndef __linux__ +extern int _ucmark(void); +#endif + +#define STKSZ (256*1024) + +static int fails; + +static void +eq(const char *what, long got, long want) +{ + if(got == want) + printf(" PASS %-46s %ld\n", what, got); + else { + printf(" FAIL %-46s got %ld want %ld\n", what, got, want); + fails++; + } +} + +/* ---- section 0 ---- */ +static int s0_count; + +/* ---- section 1 ---- */ +static ucontext_t s1_main, s1_ctx; +static char *s1_lo, *s1_hi; +static int s1_ran, s1_onstack; + +static void +s1_fn(void) +{ + char probe; + + s1_ran++; + s1_onstack = (&probe >= s1_lo && &probe < s1_hi); + swapcontext(&s1_ctx, &s1_main); +} + +/* ---- section 2 ---- */ +static ucontext_t s2_main, s2_ctx; +static int s2_ran; + +static void +s2_fn(void) +{ + s2_ran++; + /* returns: uc_link must take us back */ +} + +/* ---- section 3 ---- */ +static ucontext_t s3_main, s3_ctx; +static int s3_got[8], s3_ran; + +static void +s3_fn(int a, int b, int c, int d, int e, int f, int g, int h) +{ + s3_got[0] = a; s3_got[1] = b; s3_got[2] = c; s3_got[3] = d; + s3_got[4] = e; s3_got[5] = f; s3_got[6] = g; s3_got[7] = h; + s3_ran++; +} + +/* ---- section 4 ---- */ +static ucontext_t s4_main, s4_ctx; +static int s4_ticks; + +static void +s4_fn(void) +{ + int i; + + for(i = 0; i < 5; i++){ + s4_ticks++; + swapcontext(&s4_ctx, &s4_main); + } +} + +/* ---- section 5 ---- */ +static ucontext_t s5_main, s5_ctx; +static int s5_seen; + +static void +s5_fn(void) +{ + volatile long a = 0x1111111111111111L; + volatile long b = 0x2222222222222222L; + + swapcontext(&s5_ctx, &s5_main); + if(a == 0x1111111111111111L && b == 0x2222222222222222L) + s5_seen = 1; + swapcontext(&s5_ctx, &s5_main); +} + +static char * +newstack(ucontext_t *uc, ucontext_t *link, char **lo, char **hi) +{ + char *s = malloc(STKSZ); + + if(s == 0){ + printf(" FAIL could not allocate a stack\n"); + fails++; + exit(fails); + } + memset(s, 0, STKSZ); + getcontext(uc); + uc->uc_stack.ss_sp = s; + uc->uc_stack.ss_size = STKSZ; + uc->uc_stack.ss_flags = 0; + uc->uc_link = link; + if(lo) *lo = s; + if(hi) *hi = s + STKSZ; + return s; +} + +int +main(void) +{ + ucontext_t c0; + int i; + + /* a fault, a kill and a clean finish all give the prompt back */ + setvbuf(stdout, (char*)0, _IONBF, 0); + printf("ucontext-test: starting\n"); +#ifndef __linux__ + printf("ucontext-test: _ucmark = %d\n", _ucmark()); +#endif + + /* ---- 0: getcontext/setcontext round trip ---- */ + printf("\nSection 0: a resumed getcontext returns 0\n"); + /* + * The PASS line below prints TWICE and that is the point: the + * second is the resumed path re-entering at getcontext's return. + */ + s0_count = 0; + i = getcontext(&c0); + eq("getcontext returned 0", i, 0); + s0_count++; + if(s0_count == 1) + setcontext(&c0); /* re-enters getcontext's return */ + eq("setcontext came back round", s0_count, 2); + + /* ---- 1: a function on its own stack ---- */ + printf("\nSection 1: the function runs, on the stack it was given\n"); + newstack(&s1_ctx, &s1_main, &s1_lo, &s1_hi); + makecontext(&s1_ctx, s1_fn, 0); + swapcontext(&s1_main, &s1_ctx); + eq("the function ran", s1_ran, 1); + eq("its locals are inside the given stack", s1_onstack, 1); + + /* ---- 2: uc_link ---- */ + printf("\nSection 2: returning follows uc_link\n"); + newstack(&s2_ctx, &s2_main, (char**)0, (char**)0); + makecontext(&s2_ctx, s2_fn, 0); + swapcontext(&s2_main, &s2_ctx); + eq("the function ran", s2_ran, 1); + /* + * There is deliberately no `eq("uc_link honoured", 1, 1)' here. + * *A check that cannot fail is not a check*: if uc_link were + * ignored, `_ctxfinish' would call `exit(0)' and this line would + * never run, so REACHING section 3 is the result -- the same + * shape as the marker printed there, and the reason both are + * written as prose rather than as a passing assertion. + */ + + /* ---- 3: arguments, at the cap ---- */ + printf("\nSection 3: eight arguments, the most this port passes\n"); + printf(" (if nothing follows this line, the ucontext cell above\n"); + printf(" the argument slots was overwritten and _ctxfinish\n"); + printf(" exited: see ucontext.c's frame picture)\n"); + newstack(&s3_ctx, &s3_main, (char**)0, (char**)0); + makecontext(&s3_ctx, (void(*)(void))s3_fn, 8, + 11, 22, 33, 44, 55, 66, 77, 88); + swapcontext(&s3_main, &s3_ctx); + eq("the function ran", s3_ran, 1); + for(i = 0; i < 8; i++){ + char nm[48]; + sprintf(nm, "argument %d", i); + eq(nm, s3_got[i], 11*(i+1)); + } + + /* ---- 4: ping-pong ---- */ + printf("\nSection 4: five switches through one pair\n"); + newstack(&s4_ctx, &s4_main, (char**)0, (char**)0); + makecontext(&s4_ctx, s4_fn, 0); + for(i = 0; i < 5; i++) + swapcontext(&s4_main, &s4_ctx); + eq("ticks", s4_ticks, 5); + + /* ---- 5: locals survive a switch away and back ---- */ + printf("\nSection 5: a context's locals survive being left\n"); + newstack(&s5_ctx, &s5_main, (char**)0, (char**)0); + makecontext(&s5_ctx, s5_fn, 0); + swapcontext(&s5_main, &s5_ctx); + swapcontext(&s5_main, &s5_ctx); + eq("both locals intact after the round trip", s5_seen, 1); + + printf("\nucontext-test: %d failures\n", fails); + return fails; +} diff --git a/sys/src/ape/lib/ap/arch/amd64/mcontext.s b/sys/src/ape/lib/ap/arch/amd64/mcontext.s new file mode 100644 index 0000000000..db911b9105 --- /dev/null +++ b/sys/src/ape/lib/ap/arch/amd64/mcontext.s @@ -0,0 +1,128 @@ +/* + * 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 de7738e4f0..ee71a9156a 100644 --- a/sys/src/ape/lib/ap/arch/amd64/mkfile +++ b/sys/src/ape/lib/ap/arch/amd64/mkfile @@ -11,6 +11,8 @@ OFILES=\ main9p.$O\ notetramp.$O\ setjmp.$O\ + mcontext.$O\ + ucontext.$O\ strchr.$O\ strlen.$O\ tas.$O\ diff --git a/sys/src/ape/lib/ap/arch/amd64/ucontext.c b/sys/src/ape/lib/ap/arch/amd64/ucontext.c new file mode 100644 index 0000000000..b3db5388d1 --- /dev/null +++ b/sys/src/ape/lib/ap/arch/amd64/ucontext.c @@ -0,0 +1,220 @@ +/* + * POSIX user contexts: the C half. + * + * `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 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. + */ +#include "lib.h" +#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 NEW CONTEXT'S STACK FRAME, as `makecontext' builds it and + * `_ctxreturn' reads it back. Addresses increase downward in this + * picture; the stack itself grows the other way. + * + * base[0] the return address: _ctxreturn + * base[1] 6c's reserved 0(FP) slot for the first + * argument, which actually arrives in BP + * base[2..UC_ARGMAX] arguments 1..UC_ARGMAX-1, at 8(FP) upwards + * base[UC_ARGMAX+1] the ucontext this frame belongs to + * + * `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. + * + * ***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 + * arguments in registers the stack image does not change shape with + * `argc', so the bookkeeping cell can sit at a fixed offset whatever + * the count -- which is how glibc does it. Here only argument ZERO is + * in a register and the rest are contiguous from 8(FP), so the cell + * must come after a FIXED number of argument slots or its offset moves. + * Eight is chosen rather than derived; php passes none, and POSIX only + * promises `int' arguments. + */ +#define UC_ARGMAX 8 +#define UC_SLOTS (UC_ARGMAX + 2) + +/* + * Version marker for `ucontext-test.c'. `pcc -o x x.c' relinks against + * the INSTALLED libap, so a test built from a fresh pull can run + * days-old library code. Thirteenth use of the idiom. + */ +int +_ucmark(void) +{ + return 1; +} + +static void +ucdie(const char *msg) +{ + write(2, "makecontext: ", 13); + write(2, msg, strlen(msg)); + write(2, "\n", 1); + 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, ...) +{ + unsigned long long *base; + unsigned long long top; + va_list ap; + int i; + + if(ucp == 0) + ucdie("null context"); + if(func == 0) + ucdie("null function"); + if(argc < 0 || argc > UC_ARGMAX) + ucdie("more arguments than this port passes"); + if(ucp->uc_stack.ss_sp == 0 || ucp->uc_stack.ss_size < UC_SLOTS*8 + 64) + ucdie("stack missing or too small"); + + top = (unsigned long long)(unsigned long)ucp->uc_stack.ss_sp + + (unsigned long long)ucp->uc_stack.ss_size; + top &= ~(unsigned long long)15; /* 6c does not require 16; + * its frames are not a + * multiple of it. Free. */ + base = (unsigned long long *)(unsigned long)(top - UC_SLOTS*8); + + base[0] = (unsigned long long)(unsigned long)_ctxreturn; + base[1] = 0; /* 6c's reserved 0(FP) slot */ + for(i = 2; i <= UC_ARGMAX; i++) + base[i] = 0; + base[UC_ARGMAX+1] = (unsigned long long)(unsigned long)ucp; + + /* + * POSIX says the arguments are `int'. They are read here as the + * widest integer instead, and that is not laxity: **every vararg + * occupies eight bytes on this ABI** -- `stdarg_arch.h' advances + * the list by 8 for sizes 1, 2 and 4 alike -- so reading eight is + * reading the whole slot. A callee declaring `int' then takes the + * low four bytes and is right whatever is above them, while a + * caller that passed a POINTER (common, and what every real + * makecontext user does) is served correctly rather than + * truncated. *Reading `int' here would be the narrower choice and + * the wrong one.* + */ + va_start(ap, argc); + for(i = 0; i < argc; i++){ + unsigned long long v = (unsigned long long)va_arg(ap, long long); + if(i == 0) + ucp->uc_mcontext.mc_regs[_MC_ARG] = v; /* BP = RARG */ + else + base[i+1] = v; /* 8(FP) up */ + } + va_end(ap); + if(argc == 0) + ucp->uc_mcontext.mc_regs[_MC_ARG] = 0; + + /* + * `_setmcontext' 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. + */ + ucp->uc_mcontext.mc_regs[_MC_SP] = + (unsigned long long)(unsigned long)base - 8; + ucp->uc_mcontext.mc_regs[_MC_PC] = + (unsigned long long)(unsigned long)func; + + /* + * mc_regs[3..7] -- BX, R12..R15 -- are left exactly as they are. + * ***THAT IS WHY POSIX MAKES `getcontext' A PRECONDITION OF + * `makecontext'***: those registers may hold 6c's caches for + * global variables (REGEXT is R15, descending), so the right value + * 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. + */ +} + +/* + * Where a context's function returns to. `_ctxreturn' in the assembly + * hands over its stack pointer and nothing else; the frame layout is + * this file's, so the arithmetic is here. + */ +void +_ctxfinish(void *sp) +{ + ucontext_t *ucp; + + ucp = ((ucontext_t **)sp)[UC_ARGMAX]; + if(ucp != 0 && ucp->uc_link != 0) + setcontext(ucp->uc_link); + exit(0); +}