From dc152e575fb7e3b0d8f1acf398ecbf85a285286d Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 7 Jul 2026 21:11:28 -0500 Subject: [PATCH] GNO support fixed. --- demos/gnoConcurA.c | 55 ++++++++++++++++ demos/gnoConcurB.c | 49 ++++++++++++++ demos/gnoConcurFnptr.c | 84 ++++++++++++++++++++++++ demos/gnoConcurFork.c | 66 +++++++++++++++++++ demos/gnoForkFnptr.c | 63 ++++++++++++++++++ demos/gnoForkGlobal.c | 57 ++++++++++++++++ demos/gnoForkRace.c | 70 ++++++++++++++++++++ demos/gnoForkWork.c | 63 ++++++++++++++++++ demos/gnoHeapConcur.c | 79 ++++++++++++++++++++++ demos/rsrcProbe.apl | Bin 46706 -> 46741 bytes runtime/include/gno/kernel.h | 3 + runtime/src/crt0Gno.s | 13 ++-- runtime/src/crt0Gsos.s | 16 ++--- runtime/src/gnoGsos.s | 24 +++++++ runtime/src/gnoKernel.s | 66 +++++++++++++++++++ runtime/src/libc.c | 20 +++++- runtime/src/libcGno.c | 124 ++++++++++++++++++++++++++++++----- runtime/src/libgcc.s | 124 +++++++++++++++++++---------------- scripts/genGnoKernel.py | 5 ++ scripts/smokeTest.sh | 65 +++++++++++++++--- 20 files changed, 941 insertions(+), 105 deletions(-) create mode 100644 demos/gnoConcurA.c create mode 100644 demos/gnoConcurB.c create mode 100644 demos/gnoConcurFnptr.c create mode 100644 demos/gnoConcurFork.c create mode 100644 demos/gnoForkFnptr.c create mode 100644 demos/gnoForkGlobal.c create mode 100644 demos/gnoForkRace.c create mode 100644 demos/gnoForkWork.c create mode 100644 demos/gnoHeapConcur.c diff --git a/demos/gnoConcurA.c b/demos/gnoConcurA.c new file mode 100644 index 0000000..d524bee --- /dev/null +++ b/demos/gnoConcurA.c @@ -0,0 +1,55 @@ +// Concurrency test, program A of a pair (run "gnoconcura & gnoconcurb" under +// gsh). Proves two SEPARATE programs run CONCURRENTLY under GNO's preemptive +// scheduler via a shared-memory rendezvous that can ONLY complete if the two +// interleave: A announces itself (flagA), then spins waiting for B's flag. If A +// ran to completion before B ever started, it would time out (0xDEAD). Both +// reaching their rendezvous marker (A=0xAAAA, B=0xBBBB) means the scheduler +// actually time-sliced them. Each also does real work (sumTo) to prove it is +// not a trivial spin. Bank-2 markers are absolute physical RAM, shared across +// processes -- that is what makes the cross-process rendezvous observable. +// +// 0x025040 0001 flagA: A is alive +// 0x025044 AAAA A saw B's flag (rendezvous) AND sumTo(100)==5050; else DEAD +#include + +#define M(a) (*(volatile uint16_t *)(a)) +#define FLAG_A 0x025040UL +#define FLAG_B 0x025042UL +#define RES_A 0x025044UL +#define LIMIT 100000000UL + + +static uint16_t sumTo(uint16_t n) { + uint16_t acc; + uint16_t i; + acc = 0; + for (i = 1; i <= n; i++) { + acc = acc + i; + } + return acc; +} + + +int main(void) { + unsigned long spin; + uint16_t saw; + + M(RES_A) = 0xDEAD; + M(FLAG_A) = 0x0001; // announce A + + saw = 0; + for (spin = 0; spin < LIMIT; spin++) { + if (M(FLAG_B) == 0x0001) { + saw = 1; + break; + } + } + + if (saw == 1 && sumTo(100) == 5050) { + M(RES_A) = 0xAAAA; // rendezvous + real work OK + } + M(0x025048UL) = 0xE0AA; // A reached end (distinguishes timeout from crash) + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoConcurB.c b/demos/gnoConcurB.c new file mode 100644 index 0000000..9b9db6c --- /dev/null +++ b/demos/gnoConcurB.c @@ -0,0 +1,49 @@ +// Concurrency test, program B of a pair (see gnoConcurA.c). B announces its +// flag, spins waiting for A's, then does real work. Both markers set means the +// two SEPARATE programs genuinely time-sliced under GNO's preemptive scheduler. +// +// 0x025042 0001 flagB: B is alive +// 0x025046 BBBB B saw A's flag (rendezvous) AND sumTo(100)==5050; else DEAD +#include + +#define M(a) (*(volatile uint16_t *)(a)) +#define FLAG_A 0x025040UL +#define FLAG_B 0x025042UL +#define RES_B 0x025046UL +#define LIMIT 100000000UL + + +static uint16_t sumTo(uint16_t n) { + uint16_t acc; + uint16_t i; + acc = 0; + for (i = 1; i <= n; i++) { + acc = acc + i; + } + return acc; +} + + +int main(void) { + unsigned long spin; + uint16_t saw; + + M(RES_B) = 0xDEAD; + M(FLAG_B) = 0x0001; // announce B + + saw = 0; + for (spin = 0; spin < LIMIT; spin++) { + if (M(FLAG_A) == 0x0001) { + saw = 1; + break; + } + } + + if (saw == 1 && sumTo(100) == 5050) { + M(RES_B) = 0xBBBB; // rendezvous + real work OK + } + M(0x02504AUL) = 0xE0BB; // B reached end (distinguishes timeout from crash) + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoConcurFnptr.c b/demos/gnoConcurFnptr.c new file mode 100644 index 0000000..3406464 --- /dev/null +++ b/demos/gnoConcurFnptr.c @@ -0,0 +1,84 @@ +// The ultimate concurrency proof: parent and forked child BOTH dispatch function +// pointers WHILE preemptively interleaved. They ping-pong a shared turn word +// (proving they time-slice), and on every one of its turns each thread makes an +// indirect call through its own volatile function pointer -- the parent inc's an +// accumulator, the child dec's one. This only produces the right accumulators +// (parent 0->ROUNDS, child 1000->1000-ROUNDS) if the reentrant __jsl_indir reads +// each thread's OWN per-thread DP target: with the old shared self-modified +// operand the two threads' indirect dispatch aliased and wild-jumped. +// +// 0x025070 ROUNDS*2 (0x0014) final turn value (ping-pong completed) +// 0x025072 5050 parent did ROUNDS fnptr inc's concurrently (acc==ROUNDS) +// 0x025074 CCCC child did ROUNDS fnptr dec's concurrently (acc==1000-ROUNDS) +#include + +extern int fork(void *subr); +extern int wait(int *status); +extern __attribute__((noreturn)) void _exit(int status); + +#define M(a) (*(volatile uint16_t *)(a)) +#define TURN 0x025070UL +#define RES_P 0x025072UL +#define RES_C 0x025074UL +#define ROUNDS 10 + + +static uint16_t incf(uint16_t x) { + return x + 1; +} + + +static uint16_t decf(uint16_t x) { + return x - 1; +} + + +static uint16_t (*volatile fpP)(uint16_t) = incf; +static uint16_t (*volatile fpC)(uint16_t) = decf; + + +static void child(void) { + uint16_t k; + uint16_t acc; + acc = 1000; + for (k = 0; k < ROUNDS; k++) { + while ((M(TURN) & 1) == 0) { + } + acc = fpC(acc); // indirect call, concurrent with parent + M(TURN) = M(TURN) + 1; + } + if (acc == (uint16_t)(1000 - ROUNDS)) { + M(RES_C) = 0xCCCC; + } + _exit(0); +} + + +int main(void) { + uint16_t k; + uint16_t acc; + int pid; + int st; + + M(TURN) = 0x0000; + M(RES_P) = 0xDEAD; + M(RES_C) = 0xDEAD; + acc = 0; + + pid = fork((void *)child); + if (pid > 0) { + for (k = 0; k < ROUNDS; k++) { + while ((M(TURN) & 1) == 1) { + } + acc = fpP(acc); // indirect call, concurrent with child + M(TURN) = M(TURN) + 1; + } + if (acc == ROUNDS) { + M(RES_P) = 0x5050; + } + wait(&st); + } + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoConcurFork.c b/demos/gnoConcurFork.c new file mode 100644 index 0000000..170c30e --- /dev/null +++ b/demos/gnoConcurFork.c @@ -0,0 +1,66 @@ +// Definitive preemptive-concurrency test via fork() ping-pong. A forked GNO +// thread (child) and the parent alternate ownership of a shared "turn" word in +// bank 2: the parent advances it on EVEN turns, the child on ODD. Each must +// WAIT for the other to hand over the turn, ROUNDS times. This completes ONLY +// if GNO preemptively time-slices the two threads -- if they ran sequentially, +// whichever went first would spin forever at round 2 waiting for a turn the +// other can't give. No program-load latency and no gsh job control to muddy it +// (unlike the two-separate-programs path): fork is in-process and immediate. +// +// The child stays within the leaf-only fork contract: a register loop + absolute +// bank-2 loads/stores + _exit (no calls, libcalls, fn-pointers, or &global). +// +// 0x025050 ROUNDS*2 (0x0014) final turn value if the ping-pong completed +// 0x025052 5050 parent finished all ROUNDS turns (else DEAD = stuck/no sched) +// 0x025054 CCCC child finished all ROUNDS turns (else DEAD = never scheduled) +#include + +extern int fork(void *subr); +extern int wait(int *status); +extern __attribute__((noreturn)) void _exit(int status); + +#define M(a) (*(volatile uint16_t *)(a)) +#define TURN 0x025050UL +#define RES_P 0x025052UL +#define RES_C 0x025054UL +#define ROUNDS 10 + + +// Child (leaf): take the ODD turns. Wait until TURN is odd, then bump it even. +static void child(void) { + uint16_t k; + for (k = 0; k < ROUNDS; k++) { + while ((M(TURN) & 1) == 0) { + } + M(TURN) = M(TURN) + 1; + } + M(RES_C) = 0xCCCC; + _exit(0); +} + + +int main(void) { + uint16_t k; + int pid; + int st; + + M(TURN) = 0x0000; // even -> parent goes first + M(RES_P) = 0xDEAD; + M(RES_C) = 0xDEAD; + + pid = fork((void *)child); + if (pid <= 0) { + M(RES_P) = 0xF00D; // fork failed outright + } else { + for (k = 0; k < ROUNDS; k++) { + while ((M(TURN) & 1) == 1) { + } + M(TURN) = M(TURN) + 1; + } + M(RES_P) = 0x5050; // parent completed its turns + wait(&st); + } + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoForkFnptr.c b/demos/gnoForkFnptr.c new file mode 100644 index 0000000..466b4ca --- /dev/null +++ b/demos/gnoForkFnptr.c @@ -0,0 +1,63 @@ +// Pin down the LAST forked-child blocker: an indirect call (function pointer). +// clang routes indirect calls through __jsl_indir, whose target slot is selected +// by a single global self-modifying operand __jsl_indir_op that crt0 patches to +// the MAIN thread's DP+$B8. A forked child stores its target to its OWN (fresh) +// DP+$B8, but __jsl_indir still reads through the parent's operand -> the child's +// indirect call dispatches through the WRONG direct page. gFp is volatile so the +// call is a genuine indirect dispatch, not a devirtualized direct call. +// +// 0x025060 0001 child entered +// 0x025062 BEEF child is ABOUT to make the indirect call (crash localizer) +// 0x025064 0001 gFp(41)==42 (indirect call returned correctly) +// 0x025066 E0DD child reached end (no crash/wild-jump) +// 0x025068 5050 parent reaped the child +#include + +extern int fork(void *subr); +extern int wait(int *status); +extern __attribute__((noreturn)) void _exit(int status); + +#define M(a) (*(volatile uint16_t *)(a)) +#define BAD 0xDEAD + + +static uint16_t addOne(uint16_t x) { + return x + 1; +} + + +static uint16_t (*volatile gFp)(uint16_t) = addOne; + + +static void child(void) { + uint16_t r; + M(0x025060UL) = 0x0001; + M(0x025062UL) = 0xBEEF; // reached the indirect-call site + r = gFp(41); // indirect dispatch via __jsl_indir + if (r == 42) { + M(0x025064UL) = 0x0001; + } + M(0x025066UL) = 0xE0DD; // survived the indirect call + _exit(0); +} + + +int main(void) { + int pid; + int st; + + M(0x025060UL) = BAD; + M(0x025062UL) = BAD; + M(0x025064UL) = BAD; + M(0x025066UL) = BAD; + M(0x025068UL) = BAD; + + pid = fork((void *)child); + if (pid > 0) { + wait(&st); + M(0x025068UL) = 0x5050; + } + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoForkGlobal.c b/demos/gnoForkGlobal.c new file mode 100644 index 0000000..54443f8 --- /dev/null +++ b/demos/gnoForkGlobal.c @@ -0,0 +1,57 @@ +// Confirm the &global blocker for forked children: forming a 32-bit &global +// pointer reads DP $BE for the bank byte, and a forked child's fresh DP has $BE +// uninitialized (crt0 sets it only for the main thread). So a child that takes +// the address of a global and dereferences it through a real pointer should read +// from the WRONG bank -> garbage (or crash), unless/until a fork thunk sets $BE. +// Passing &gCanary through a noinline fn forces a genuine 32-bit pointer (not a +// near/absolute access the compiler could fold). +// +// 0x025060 0001 child entered +// 0x025062 xxxx value read back through &gCanary (0x1234 == $BE correct) +// 0x025064 E0DD child reached end (no crash) +// 0x025066 5050 parent reaped the child +#include + +extern int fork(void *subr); +extern int wait(int *status); +extern __attribute__((noreturn)) void _exit(int status); + +#define M(a) (*(volatile uint16_t *)(a)) +#define BAD 0xDEAD + +static volatile uint16_t gCanary = 0x1234; + + +static uint16_t __attribute__((noinline)) deref16(volatile uint16_t *p) { + return *p; // deref a full 32-bit pointer +} + + +static void child(void) { + uint16_t v; + M(0x025060UL) = 0x0001; + v = deref16((volatile uint16_t *)&gCanary); // &global -> uses $BE bank byte + M(0x025062UL) = v; // 0x1234 if $BE right; else garbage + M(0x025064UL) = 0xE0DD; + _exit(0); +} + + +int main(void) { + int pid; + int st; + + M(0x025060UL) = BAD; + M(0x025062UL) = BAD; + M(0x025064UL) = BAD; + M(0x025066UL) = BAD; + + pid = fork((void *)child); + if (pid > 0) { + wait(&st); + M(0x025066UL) = 0x5050; + } + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoForkRace.c b/demos/gnoForkRace.c new file mode 100644 index 0000000..5539b93 --- /dev/null +++ b/demos/gnoForkRace.c @@ -0,0 +1,70 @@ +// Stress the fork stash race (fix for the rapid-double-fork window). The parent +// forks FOUR children back-to-back, each a DIFFERENT function that writes its own +// signature to its own slot. Without the fork mutex + consumption handshake, a +// second fork() would overwrite __gnoPendingChildFn before the first child read +// it, so a child would run the WRONG entry -- a slot would be missing or double- +// written. With the handshake, fork() blocks until each child has consumed the +// stash, so every child runs its intended function. +// +// 0x025090 AAAA childA ran (else DEAD -> A got the wrong entry) +// 0x025092 BBBB childB ran +// 0x025094 CCCC childC ran +// 0x025096 DDDD childD ran +// 0x025098 E0DD parent reaped all four +#include + +extern int fork(void *subr); +extern int wait(int *status); +extern __attribute__((noreturn)) void _exit(int status); + +#define M(a) (*(volatile uint16_t *)(a)) + + +static void childA(void) { + M(0x025090UL) = 0xAAAA; + _exit(0); +} + + +static void childB(void) { + M(0x025092UL) = 0xBBBB; + _exit(0); +} + + +static void childC(void) { + M(0x025094UL) = 0xCCCC; + _exit(0); +} + + +static void childD(void) { + M(0x025096UL) = 0xDDDD; + _exit(0); +} + + +int main(void) { + int st; + + M(0x025090UL) = 0xDEAD; + M(0x025092UL) = 0xDEAD; + M(0x025094UL) = 0xDEAD; + M(0x025096UL) = 0xDEAD; + M(0x025098UL) = 0xDEAD; + + fork((void *)childA); // back-to-back: stresses the stash window + fork((void *)childB); + fork((void *)childC); + fork((void *)childD); + + wait(&st); + wait(&st); + wait(&st); + wait(&st); + + M(0x025098UL) = 0xE0DD; + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoForkWork.c b/demos/gnoForkWork.c new file mode 100644 index 0000000..0125f5a --- /dev/null +++ b/demos/gnoForkWork.c @@ -0,0 +1,63 @@ +// Map a forked child's REAL capabilities on a fresh DP (concurrency step 2). +// The leaf-only contract forbids "i32/i64 libcalls" -- but libcalls use the +// child's OWN $E0-$FF DP scratch (write-before-read), which a fresh page +// provides, so a child that CALLS A FUNCTION doing an i32 multiply (a real +// __mulsi3 libcall) may already work without any DP-scratch setup. This probe +// tests exactly that: a non-leaf child that JSLs a helper which does a genuine +// 32-bit multiply. It deliberately does NOT touch &global (needs $BE) or a +// function pointer (needs $B8 / the __jsl_indir operand) -- those are tested +// separately, since they can wild-jump. Reading a global's VALUE (gA/gB) is +// plain absolute/DBR addressing, not a 32-bit &global pointer. +// +// 0x025060 0001 child entered +// 0x025062 0001 mul32(gA,gB) == 300000 (function call + i32 libcall worked) +// 0x025064 E0DD child reached end (no crash) +// 0x025066 5050 parent reaped the child +#include + +extern int fork(void *subr); +extern int wait(int *status); +extern __attribute__((noreturn)) void _exit(int status); + +#define M(a) (*(volatile uint16_t *)(a)) +#define BAD 0xDEAD + +static volatile uint32_t gA = 100000; +static volatile uint32_t gB = 3; + + +static uint32_t __attribute__((noinline)) mul32(uint32_t x, uint32_t y) { + return x * y; // real __mulsi3 libcall (uses $E0-$FF) +} + + +static void child(void) { + uint32_t r; + M(0x025060UL) = 0x0001; // child entered on its fresh DP + r = mul32(gA, gB); // fn-call + i32 libcall + if (r == 300000UL) { + M(0x025062UL) = 0x0001; + } + M(0x025064UL) = 0xE0DD; // reached end -> no crash + _exit(0); +} + + +int main(void) { + int pid; + int st; + + M(0x025060UL) = BAD; + M(0x025062UL) = BAD; + M(0x025064UL) = BAD; + M(0x025066UL) = BAD; + + pid = fork((void *)child); + if (pid > 0) { + wait(&st); + M(0x025066UL) = 0x5050; + } + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoHeapConcur.c b/demos/gnoHeapConcur.c new file mode 100644 index 0000000..0229a70 --- /dev/null +++ b/demos/gnoHeapConcur.c @@ -0,0 +1,79 @@ +// Stress concurrent malloc/free (fix: the __heapLock semaphore mutex). The +// parent and a forked child both hammer the SHARED heap -- each repeatedly +// malloc()s a block, fills it with a thread-unique pattern, holds it briefly (to +// invite preemption), verifies the pattern survived, and free()s it. If the +// free-list critical section were not serialized, the two threads' interleaved +// malloc/free could hand the same block to both (or corrupt the list), so a +// pattern check would fail or the walk would crash. Both loops completing with +// intact patterns shows the heap is concurrency-safe. +// +// 0x025082 AAAA parent's alloc loop finished with every pattern intact +// 0x025084 CCCC child's alloc loop finished with every pattern intact +// 0x025086 E0DD parent reaped the child +#include + +extern int fork(void *subr); +extern int wait(int *status); +extern __attribute__((noreturn)) void _exit(int status); +extern void *malloc(unsigned long n); +extern void free(void *p); + +#define M(a) (*(volatile uint16_t *)(a)) +#define ITERS 120 + + +static uint16_t allocLoop(uint16_t tag) { + volatile uint16_t *p; + uint16_t ok; + int i; + int j; + + ok = 1; + for (i = 0; i < ITERS; i++) { + p = (volatile uint16_t *)malloc(32); + if (p == 0) { + ok = 0; + break; + } + for (j = 0; j < 16; j++) { + p[j] = (uint16_t)(tag + j); + } + for (volatile int s = 0; s < 40; s++) { // hold the block; invite a task swap + } + for (j = 0; j < 16; j++) { + if (p[j] != (uint16_t)(tag + j)) { + ok = 0; + } + } + free((void *)p); + } + return ok; +} + + +static void child(void) { + uint16_t ok; + ok = allocLoop(0xC000); + M(0x025084UL) = ok ? 0xCCCC : 0xDEAD; + _exit(0); +} + + +int main(void) { + uint16_t ok; + int st; + + M(0x025082UL) = 0xDEAD; + M(0x025084UL) = 0xDEAD; + M(0x025086UL) = 0xDEAD; + + fork((void *)child); + ok = allocLoop(0xA000); // parent hammers the heap concurrently + M(0x025082UL) = ok ? 0xAAAA : 0xDEAD; + + wait(&st); + M(0x025086UL) = 0xE0DD; + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/rsrcProbe.apl b/demos/rsrcProbe.apl index b9cb8d2e856a24f500a3d6bb0b67ade543424a11..3fe541a8dcf4815eb4e96d28878dbe2b87061bfc 100644 GIT binary patch delta 5229 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 QU}$4F$#5E= X rtl +; Kscreate(i16, i32) -> i16 +; toolset 3, func 0x0F03 + .section .text.Kscreate,"ax" + .globl Kscreate +Kscreate: + ; --- stash arg0 (in A) --- + sta 0xE0 + ; --- result space (2 bytes) --- + pea 0 + ; --- arg0 --- + lda 0xE0 + pha + ; --- arg1 (4B) --- + lda 10, s + pha + lda 10, s + pha + ldx #0x0F03 + jsl 0xe10008 + pla ; result lo -> A + rtl + +; Kswait(i16, i32) -> i16 +; toolset 3, func 0x0D03 + .section .text.Kswait,"ax" + .globl Kswait +Kswait: + ; --- stash arg0 (in A) --- + sta 0xE0 + ; --- result space (2 bytes) --- + pea 0 + ; --- arg0 --- + lda 0xE0 + pha + ; --- arg1 (4B) --- + lda 10, s + pha + lda 10, s + pha + ldx #0x0D03 + jsl 0xe10008 + pla ; result lo -> A + rtl + +; Kssignal(i16, i32) -> i16 +; toolset 3, func 0x0E03 + .section .text.Kssignal,"ax" + .globl Kssignal +Kssignal: + ; --- stash arg0 (in A) --- + sta 0xE0 + ; --- result space (2 bytes) --- + pea 0 + ; --- arg0 --- + lda 0xE0 + pha + ; --- arg1 (4B) --- + lda 10, s + pha + lda 10, s + pha + ldx #0x0E03 + jsl 0xe10008 + pla ; result lo -> A + rtl + ; Kdup(i16, i32) -> i16 ; toolset 3, func 0x2203 .section .text.Kdup,"ax" diff --git a/runtime/src/libc.c b/runtime/src/libc.c index 1277b21..467f8aa 100644 --- a/runtime/src/libc.c +++ b/runtime/src/libc.c @@ -623,6 +623,12 @@ void __heapInitMM(unsigned short userID) { heapSetWindow(base, base + want); } +// Heap serialization hooks -- weak no-ops (libgcc.s) for single-threaded +// runtimes; libcGno.c overrides them with a GNO binary-semaphore mutex so +// malloc/free are safe across preemptive threads. +extern void __heapLock(void); +extern void __heapUnlock(void); + void *malloc(size_t n0) { mallocInitOnce(); // Heap ceiling is ~32KB so anything > 0x7FF0 is unsatisfiable. @@ -639,7 +645,10 @@ void *malloc(size_t n0) { n = (u16)((n + 1) & ~(u16)1); if (n < (u16)(FREE_NODE_SZ - HDR_SZ)) n = (u16)(FREE_NODE_SZ - HDR_SZ); - // First-fit on free list. + // First-fit on free list. __heapLock/__heapUnlock serialize this free-list + // critical section against concurrent threads (a GNO semaphore mutex under + // libcGno.c; a no-op single-threaded). + __heapLock(); FreeBlk **link = &freeList; FreeBlk *cur = freeList; while (cur) { @@ -655,6 +664,7 @@ void *malloc(size_t n0) { } else { *link = cur->next; } + __heapUnlock(); return (char *)cur + HDR_SZ; } link = &cur->next; @@ -677,14 +687,19 @@ void *malloc(size_t n0) { // and every p share a bank and heapEnd >= p -- so it is a small // non-negative remaining-byte count. Covered by smoke check #176. char *p = bumpPtr; - if ((size_t)(HDR_SZ + n) > (size_t)(heapEnd - p)) return (void *)0; + if ((size_t)(HDR_SZ + n) > (size_t)(heapEnd - p)) { + __heapUnlock(); + return (void *)0; + } *(u16 *)p = (u16)n; bumpPtr = p + HDR_SZ + n; + __heapUnlock(); return p + HDR_SZ; } void free(void *p) { if (!p) return; + __heapLock(); FreeBlk *blk = (FreeBlk *)((char *)p - HDR_SZ); blk->next = freeList; freeList = blk; @@ -731,6 +746,7 @@ void free(void *p) { a = a->next; } } + __heapUnlock(); } void *calloc(size_t nmemb, size_t size) { diff --git a/runtime/src/libcGno.c b/runtime/src/libcGno.c index da6285e..b3a3b6b 100644 --- a/runtime/src/libcGno.c +++ b/runtime/src/libcGno.c @@ -306,30 +306,120 @@ static int gnoToPosixErrno(int e) { // fork: GNO thread-spawn -- NOT Unix copy-fork. `subr` is the 24-bit address of -// a function that runs in the CHILD on a fresh stack + direct page; the parent -// gets the child pid, the child never returns to the call site. GNO's createProc -// (kern/gno/sys.c) starts the child at subr with DBR=PBR=subr's bank (= our -// program bank, since &func is loader-relocated) but a FRESH direct page -- so the -// crt0-initialized DP scratch ($B8 __jsl_indir vector, $BE &global bank byte, -// $E0-$FF libcall scratch) is ABSENT. `subr` must therefore point at a LEAF that -// uses only absolute-long addressing and ends in _exit()/exec*(): no function -// pointers, no i32/i64 libcalls, no &global 32-bit pointers, and it must not -// return. (A future __gnoForkThunk could relax this, but the single shared -// __jsl_indir_op operand caps how far an intra-image child can go regardless.) +// a function that runs in the CHILD as a concurrent thread on a fresh stack + +// direct page; the parent gets the child pid. GNO's createProc (kern/gno/sys.c) +// starts the child with DBR=PBR=program bank but a FRESH direct page whose codegen +// scratch is uninitialized. We spawn __gnoForkThunk (gnoGsos.s) instead of subr +// directly: it seeds the child's per-thread $BE (the &global bank byte crt0 sets +// for the main thread) and then dispatches to subr. Together with the reentrant +// __jsl_indir (per-thread indirect-call dispatch, no shared self-modified +// operand), a forked child is now a FULL-CAPABILITY thread -- function calls, +// i32/i64 libcalls, &global pointers, function pointers, and syscalls all work, +// concurrently with the parent. ($E0-$FF libcall scratch and $B8/$BA per-call +// indirect target are written-before-read, so a fresh page needs no init for +// them.) The child may return (the thunk _exit(0)s it) or _exit/exec itself. // -// `subr` is typed void* (matching GNO's own `int fork(void *subr)` and Kfork's -// arg type), NOT a function-pointer type: the callee-side lowering of a -// function-pointer PARAMETER currently drops the value (it saves only the low -// word and never reloads it, so Kfork sees subr==0), whereas the ptr32 data -// path carries the full 24-bit address. Callers pass (void *)&fn. +// Not yet concurrency-safe: the malloc heap is shared with the parent and its +// allocator is not serialized, so a child doing heavy allocation must not race +// the parent's; and __gnoPendingChildFn is a single global, so forking a second +// child before the first is scheduled would race (GNO programs fork one at a time). +// +// `subr` is typed void* (matching GNO's own `int fork(void *subr)`), NOT a +// function-pointer type: the callee-side lowering of a function-pointer PARAMETER +// currently drops the value (saves only the low word, never reloads it), whereas +// the ptr32 data path carries the full 24-bit address. Callers pass (void *)&fn. +void *__gnoPendingChildFn; // subr for the current child; read by __gnoChildStart +extern void __gnoForkThunk(void); + +// Fork serialization + stash handoff. __forkMutex serializes concurrent fork() +// calls; __forkDone lets the child signal that it has read __gnoPendingChildFn, so +// the parent can release the mutex and let the next fork proceed. Without it a +// rapid second fork could overwrite the stash before the first child read it. +// Both are GNO binary semaphores, lazily created on the first fork (the program +// is single-threaded until the first child spawns). +// Concurrency state, all lazily set up by the FIRST fork() (which runs while the +// program is still single-threaded). __gnoThreaded gates both the fork +// serialization AND the heap lock: before any fork there is only one thread, so +// malloc/free need no locking and pay nothing -- no semaphore syscall. +static int __forkMutex = 0; // serialize concurrent fork() calls +static int __forkDone = 0; // child -> parent "stash consumed" handshake +static int __heapSem = 0; // malloc/free free-list mutex (see __heapLock) +static int __gnoThreaded = 0; // 0 until the first fork(); gates all of the above + int fork(void *subr) { int err = 0; - int r = Kfork(subr, &err); - if (r < 0) errno = gnoToPosixErrno(err); + int r; + if (__gnoThreaded == 0) { + // First fork(): lazily create the concurrency semaphores while the + // program is still single-threaded. All three gate concurrency + // safety (fork serialization + heap locking), so commit to threaded + // mode ONLY if every create succeeds -- latching __gnoThreaded on a + // failed create would silently disable every guard. On failure (e.g. + // the system semaphore table is exhausted) K* returns -1 with err set; + // fail the fork with errno and stay single-threaded so malloc/free + // keep their safe single-threaded early-out. + __forkMutex = Kscreate(1, &err); // serialize forks + __forkDone = Kscreate(0, &err); // fork handshake + __heapSem = Kscreate(1, &err); // heap mutex + if (__forkMutex < 0 || __forkDone < 0 || __heapSem < 0) { + errno = gnoToPosixErrno(err); + return -1; + } + __gnoThreaded = 1; // enable locking from here on + } + Kswait(__forkMutex, &err); // serialize against other fork()s + __gnoPendingChildFn = subr; + r = Kfork((void *)__gnoForkThunk, &err); + if (r < 0) { + Kssignal(__forkMutex, &err); + errno = gnoToPosixErrno(err); + return r; + } + Kswait(__forkDone, &err); // block until the child consumes the stash + Kssignal(__forkMutex, &err); // release; the next fork may proceed return r; } +// __gnoChildStart: runs on the CHILD thread, called by __gnoForkThunk (gnoGsos.s) +// after it seeds the child's per-thread $BE. Reads the stashed child entry, +// signals the parent's fork() that the stash is consumed (so the next fork may +// proceed), then runs the child. The indirect call goes through the reentrant +// __jsl_indir on this child's OWN direct page, so the child is a full-capability +// concurrent thread. If the child returns, _exit(0). +void __gnoChildStart(void) { + void (*fn)(void); + int err = 0; + fn = (void (*)(void))__gnoPendingChildFn; + Kssignal(__forkDone, &err); // stash consumed -> unblock parent fork() + fn(); // run the real child concurrently + _exit(0); +} + + +// __heapLock / __heapUnlock: GNO override of the weak no-ops in libgcc.s. The +// binary-semaphore mutex __heapSem (created by the first fork) serializes +// malloc/free's free-list critical section across preemptive threads. Until the +// first fork the program is single-threaded, so both are a cheap early-out -- a +// single-threaded GNO program pays no semaphore syscall per malloc/free. +void __heapLock(void) { + int err = 0; + if (__gnoThreaded == 0) { + return; + } + Kswait(__heapSem, &err); +} + + +void __heapUnlock(void) { + int err = 0; + if (__gnoThreaded == 0) { + return; + } + Kssignal(__heapSem, &err); +} + + // wait: block until a child terminates; *status receives the 16-bit union-wait // word (decode with WIFEXITED/WEXITSTATUS from ). Returns the child // pid, or -1 with errno set. Kwait(*status, *errno) -- the kernel writes the diff --git a/runtime/src/libgcc.s b/runtime/src/libgcc.s index 47e01fe..7d390c8 100644 --- a/runtime/src/libgcc.s +++ b/runtime/src/libgcc.s @@ -22,56 +22,44 @@ .text ; -------------------------------------------------------------------- -; Indirect-call trampoline. An indirect call (function pointer) stores -; the target's 16-bit address to __indirTarget before JSL'ing here. -; This routine does a JMP indirect through that variable: control -; transfers to the target with the original caller's JSL frame still -; on the stack, so target's RTL returns to the original caller (one -; frame, no double-RTL). +; Indirect-call trampoline (REENTRANT). An indirect call (function +; pointer) stores its 24-bit target -- 16-bit offset to __indirTarget +; ($B8/$B9) and bank byte to $BA -- then JSLs here. __jsl_indir reads +; that target DP-RELATIVE and RTL-jumps to it with the original caller's +; JSL frame still on the stack, so the target's own RTL returns straight +; back to the caller (one frame, no double-RTL). The instruction-level +; idiom is documented at the __jsl_indir label below. ; ; Caller emit sequence in W65816ISelLowering::LowerCall: -; sta __indirTarget ; store ptr (must precede any A clobber for args) -; ... arg pushes ... +; sta __indirTarget ; store ptr offset (must precede any A clobber for args) +; ...store target bank to $BA, arg pushes... ; jsl __jsl_indir ; -; __indirTarget is a DP-relative slot at offset $B8. Callers store the -; target's 16-bit address with `sta $B8` (DP-form, writes to (DP+$B8) -; in bank 0). The trampoline below is a JMP (abs) whose operand has -; been patched by the crt0 variant at startup to point at the runtime -; DP+$B8 anchor; the JSL → JMP → target flow then takes a single CPU -; instruction past the JSL, so A and X (which hold arg0 for the 65816 -; C ABI) survive the indirection unmodified. +; No self-modified operand: the target is read from the caller's OWN +; direct page ($B8/$B9 offset + $BA bank), and DP is per-thread -- GNO +; context-switches it every task swap, and both the GS/OS Loader and GNO +; keep DP at a non-zero, process-private page (never bank-0 zero page). +; So each thread dispatches through ITS OWN target, and a fork()ed child's +; function pointer no longer wild-jumps through the parent thread's page. +; (An earlier form JMP'd through a single global operand that crt0 self- +; modified to the main thread's DP+$B8; it aliased under preemptive multi- +; tasking and is gone -- there is no __jsl_indir_op now, and the crt0 +; variants no longer patch anything.) ; -; Why the SMC dispatch instead of a fixed `jmp ($00B8)`: -; Under bare-metal (crt0.s) the runtime leaves DP=0, so the patched -; operand stays $00B8 and the dispatch is byte-identical to the legacy -; fixed form. Under the GS/OS Loader (crt0Gsos.s) AND GNO/ME -; (crt0Gno.s) we keep DP at a non-zero value -- the Loader-allocated -; page under GS/OS, GNO's per-process page under GNO -- and patch the -; operand to (DP + $B8). That keeps every codegen scratch slot (the -; indirect-call anchor at $B8, the PBR stash at $BE, the libcall -; scratch at $E0..$FF) in reserved / process-private bank-0 memory -; instead of on top of system zero page that ROM IRQ handlers and -; GS/OS dispatcher state freely scribble (and, under GNO's preemptive -; multitasking, that a concurrent second copy of ours would alias). -; Forcing DP=0 was making clang-built S16 apps with C++ ctors crash -; during the init_array indirect-dispatch loop for exactly that reason. +; Keeping DP non-zero also parks every codegen scratch slot (indirect-call +; anchor $B8, PBR stash $BE, libcall scratch $E0..$FF) in reserved / +; process-private bank-0 memory instead of on system zero page that ROM +; IRQ handlers and GS/OS dispatcher state freely scribble. Forcing DP=0 +; used to crash clang-built S16 apps with C++ ctors in the init_array +; indirect-dispatch loop for exactly that reason. DP is always in bank 0 +; on the 65816, so the $B8/$BA reads resolve to bank 0 regardless of DP -- +; bare-metal (crt0.s) keeps DP=0 and the same reads land at $00B8/$00BA, +; needing no patch. ; -; Constraint preserved: the long-indirect anchor must be in bank 0 (the -; 65816 JML [abs] opcode 0xDC reads its 24-bit PC vector from bank-0 -; unconditionally). DP is always in bank 0 on the 65816, so -; (DP+$B8) is automatically a bank-0 address. The fetched 24-bit -; pointer (offset $B8-$B9 + bank $BA, both stored by clang at the call -; site) is honored BANK-EXPLICIT, so a cross-bank function pointer -- -; e.g. &func of a function the linker placed in a different segment/ -; bank, redirected through a seg1 JML trampoline -- lands in the -; correct bank. When bank==PBR (single-segment) this is byte-for-byte -; equivalent to the old 0x6C/16-bit dispatch. -; -; SMC patch site: crt0 variants compute (DP + $B8) at startup with -; tdc ; clc ; adc #0xb8 ; sta __jsl_indir_op -; The default operand below is $00B8 so a program that links without -; patching (e.g. an asm-only test) still works under DP=0. +; The fetched pointer is honored BANK-EXPLICIT via the $BA bank byte, so a +; cross-bank function pointer -- e.g. &func the linker placed in a +; different segment/bank and redirected through a seg1 JML trampoline -- +; lands in the correct bank. ; -------------------------------------------------------------------- .globl __indirTarget __indirTarget = 0xb8 @@ -79,19 +67,26 @@ __indirTarget = 0xb8 .text .globl __jsl_indir __jsl_indir: - .byte 0xdc ; JML [abs] opcode -- 24-bit LONG indirect. Reads a - ; 24-bit pointer (offset $B8-$B9 + bank $BA) from - ; bank-0:operand and jumps BANK-EXPLICIT, so a - ; cross-bank function pointer lands in the right bank. - ; (Was 0x6C JMP(abs), 16-bit/PBR-local; clang already - ; stores the pointer's bank byte to $BA, so this just - ; honors it. Identical to the old form when bank==PBR.) - .globl __jsl_indir_op -__jsl_indir_op: - .byte 0xb8, 0x00 ; operand: bank-0 address of the target slot. - ; Patched at startup by crt0 to (DP + $B8). The - ; default $00B8 is correct only under bare-metal - ; DP=0; GS/OS and GNO patch it to their non-zero DP. + ; REENTRANT dispatch: read the 24-bit target DP-RELATIVE (offset $B8-$B9, + ; bank $BA -- both stored by clang at the call site) and RTL to it, with NO + ; self-modified global operand. DP is per-thread (context-switched by GNO + ; every task swap; process-private under GS/OS), so each caller reads ITS + ; OWN target -- a fork()ed child dispatching a function pointer no longer + ; wild-jumps through the parent thread's page. Preserves A/X/Y (C-ABI arg + ; regs) and the caller's JSL frame (the target's RTL returns to the JSL + ; site). $BC is a per-thread DP temp to free A; assumes the 16-bit native + ; call state (M=0/X=0) every clang indirect call + the ctor loop use. RTL + ; adds 1 to the pulled 16-bit PC, hence offset-1. + sta 0xbc ; save arg0 (A) to a per-thread DP temp + sep #0x20 ; 8-bit A for the 1-byte bank push + lda 0xba ; target bank + pha ; push PBR + rep #0x20 ; back to 16-bit A + lda 0xb8 ; target offset (16-bit) + dec a ; offset-1 (RTL adds 1 to the pulled PC) + pha ; push PCH:PCL + lda 0xbc ; restore arg0 (A) + rtl ; -> bank:offset; caller's RTL frame remains below ; -------------------------------------------------------------------- ; __run_cxa_atexit — weak no-op fallback. @@ -105,6 +100,21 @@ __jsl_indir_op: __run_cxa_atexit: rtl +; -------------------------------------------------------------------- +; __heapLock / __heapUnlock -- weak no-op heap-serialization hooks. +; +; malloc()/free() (libc.c) bracket their free-list critical sections with these. +; Single-threaded runtimes (bare-metal crt0.s, GS/OS crt0Gsos.s) keep the no-op: +; there are no concurrent threads to serialize. libcGno.c overrides both with a +; GNO binary-semaphore mutex so malloc/free are safe across preemptive threads. +; -------------------------------------------------------------------- + .weak __heapLock +__heapLock: + rtl + .weak __heapUnlock +__heapUnlock: + rtl + ; -------------------------------------------------------------------- ; __srandInitFromTime — weak no-op fallback. ; diff --git a/scripts/genGnoKernel.py b/scripts/genGnoKernel.py index 92ef079..dcca11c 100644 --- a/scripts/genGnoKernel.py +++ b/scripts/genGnoKernel.py @@ -48,6 +48,11 @@ SYSCALLS = [ (0x1B03, "Ksigsetmask", 4, [4, 4]), # (mask, *errno) -> longword (0x1C03, "Ksigblock", 4, [4, 4]), # (mask, *errno) -> longword + # ---- semaphores (binary-mutex + handshake for thread-safe malloc/fork) ---- + (0x0F03, "Kscreate", 2, [2, 4]), # (count, *errno) -> sem id + (0x0D03, "Kswait", 2, [2, 4]), # (sem, *errno) -> int (P / lock) + (0x0E03, "Kssignal", 2, [2, 4]), # (sem, *errno) -> int (V / unlock) + # ---- File descriptors ---- (0x2203, "Kdup", 2, [2, 4]), # (oldfd, *errno) -> int (0x2303, "Kdup2", 2, [2, 2, 4]), # (oldfd, newfd, *errno) -> int diff --git a/scripts/smokeTest.sh b/scripts/smokeTest.sh index 8fe7056..f903868 100755 --- a/scripts/smokeTest.sh +++ b/scripts/smokeTest.sh @@ -6765,15 +6765,16 @@ EOF "${binPhc2%.bin}".seg*.bin "${relPhc2}".seg*.reloc fi -# crt0Gsos indirect-dispatch regression: ensure the __jsl_indir SMC patch -# (`tdc; clc; adc #$B8; sta __jsl_indir_op` in crt0Gsos.s) lets a clang- -# built S16 app survive an init_array-style indirect call under the -# real GS/OS Loader. Before the fix, crt0Gsos forced DP=0 and the -# indirect dispatcher hard-coded $00B8 — but bank-0 zero page is -# scribbled by ROM IRQ handlers + GS/OS dispatcher state, so the first -# indirect call (C++ static ctor, etc.) raced and crashed. The probe -# below uses a function-pointer table to force the codegen indirect -# path through __jsl_indir. Marker $70 = 0x0B = twice(7) + neg(3). +# crt0Gsos indirect-dispatch regression: ensure a clang-built S16 app +# survives an init_array-style indirect call under the real GS/OS Loader. +# crt0Gsos keeps DP at the Loader's non-zero page and __jsl_indir reads +# its 24-bit target DP-RELATIVE ($B8/$BA), so the indirect anchor stays in +# the Loader-reserved page. An earlier form forced DP=0, putting the +# anchor at $00B8 on bank-0 zero page -- scribbled by ROM IRQ handlers + +# GS/OS dispatcher state -- so the first indirect call (C++ static ctor, +# etc.) raced and crashed. The probe below uses a function-pointer table +# to force the codegen indirect path through __jsl_indir. Marker +# $70 = 0x0B = twice(7) + neg(3). if [ "${SMOKE_SKIP_GSOSINDIR:-0}" != "1" ] \ && [ -x "$CLANG" ] && [ -x "$CADIUS" ] && [ -f "$SYSDISK" ] \ && command -v mame >/dev/null 2>&1; then @@ -7038,6 +7039,52 @@ else } log "OK: gnoForkWaitProbe fork/wait + _exit status (WEXITSTATUS=42) green under GNO" + # Phase 5.3d.2 CONCURRENCY: a forked child is a full-capability concurrent + # thread, not a leaf. Three guards: + # - gnoConcurFork: parent+child ping-pong a shared turn word ROUNDS times + # each (0x025050=0x14) -- only completes if GNO preemptively time-slices + # them; sequential execution would deadlock. Proves preemptive multitasking. + # - gnoForkGlobal: a child dereferences a &global 32-bit pointer (reads + # 0x1234); needs __gnoForkThunk to seed the child's per-thread $BE bank + # byte -- a fresh fork DP has garbage there. + # - gnoConcurFnptr: parent AND child dispatch function pointers WHILE + # interleaved (0x025072=5050 + 0x025074=CCCC). Guards the reentrant + # __jsl_indir: the old shared self-modified operand made a child's indirect + # call wild-jump through the parent's direct page. + # - gnoForkRace: parent forks FOUR distinct children back-to-back; each runs + # its intended function only if the fork mutex + consumption handshake + # close the __gnoPendingChildFn stash race. + # - gnoHeapConcur: parent + child both hammer malloc/free on the shared heap + # 120x each with pattern checks -- passes only if __heapLock (GNO semaphore + # mutex) serializes the free-list critical section. + for concSpec in \ + "gnoConcurFork|preemptive ping-pong|0x025050=0014 0x025052=5050 0x025054=CCCC" \ + "gnoForkGlobal|child &global via thunk \$BE|0x025062=1234 0x025064=E0DD 0x025066=5050" \ + "gnoConcurFnptr|concurrent fnptr dispatch|0x025070=0014 0x025072=5050 0x025074=CCCC" \ + "gnoForkRace|rapid multi-fork stash handoff|0x025090=AAAA 0x025092=BBBB 0x025094=CCCC 0x025096=DDDD 0x025098=E0DD" \ + "gnoHeapConcur|concurrent malloc/free heap mutex|0x025082=AAAA 0x025084=CCCC 0x025086=E0DD" \ + ; do + concProbe="${concSpec%%|*}" + concRest="${concSpec#*|}" + concDesc="${concRest%%|*}" + concChecks="${concRest##*|}" + concArgs="" + for concChk in $concChecks; do + concArgs="$concArgs --check $concChk" + done + log "check: $concProbe ($concDesc) under GNO" + bash "$PROJECT_ROOT/demos/buildGno.sh" "$concProbe" >/tmp/gnoConcBuildOut 2>&1 || { + cat /tmp/gnoConcBuildOut >&2 + die "buildGno.sh $concProbe failed" + } + bash "$PROJECT_ROOT/scripts/runInGno.sh" "$PROJECT_ROOT/demos/$concProbe.omf" $concArgs \ + >/tmp/gnoConcRunOut 2>&1 || { + cat /tmp/gnoConcRunOut >&2 + die "$concProbe failed: GNO concurrency regression ($concDesc)" + } + log "OK: $concProbe ($concDesc) green under GNO" + done + # Phase 5.3e..5.3k POSIX syscall surface (punch-list items 4-12): the GNO # kernel toolset-$03 wrappers (waitpid/stat/isatty/uid/gid/job-control/env/ # fd layer/exec/signals) now wired into libc via libcGno.c. Each probe