From 8d30c0660583006b8a41feef2e2dbed546713e2f Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 7 Jul 2026 18:33:48 -0500 Subject: [PATCH] Another pass at GNO. --- demos/buildGno.sh | 14 +- demos/gnoBigBssProbe.c | 29 + demos/gnoComposite.c | 199 +++ demos/gnoEnvProbe.c | 42 + demos/gnoErrnoProbe.c | 93 ++ demos/gnoExecDiag.c | 65 + demos/gnoExecProbe.c | 81 ++ demos/gnoExeced.c | 21 + demos/gnoFdProbe.c | 103 ++ demos/gnoForkWaitProbe.c | 56 + demos/gnoHeapProbe.c | 41 + demos/gnoIdProbe.c | 47 + demos/gnoSignalProbe.c | 43 + demos/gnoStatProbe.c | 72 + demos/gnoWaitpidTimesProbe.c | 59 + demos/rsrcProbe.apl | Bin 46685 -> 46706 bytes runtime/include/fcntl.h | 49 + runtime/include/signal.h | 46 + runtime/include/stdlib.h | 11 +- runtime/include/sys/stat.h | 100 ++ runtime/include/sys/times.h | 25 + runtime/include/sys/types.h | 20 + runtime/include/sys/wait.h | 28 + runtime/include/unistd.h | 87 ++ runtime/src/crt0Gno.s | 128 +- runtime/src/crt0Gsos.s | 8 +- runtime/src/gnoGsos.s | 132 +- runtime/src/gnoKernel.s | 72 +- runtime/src/libc.c | 40 +- runtime/src/libcGno.c | 1401 +++++++++++++++++++- runtime/src/libgcc.s | 27 +- scripts/genGnoKernel.py | 15 +- scripts/runInGno.sh | 7 + scripts/smokeTest.sh | 171 +++ src/link816/omfEmit.cpp | 2 +- src/llvm/lib/Target/W65816/W65816UnLSR.cpp | 23 +- 36 files changed, 3198 insertions(+), 159 deletions(-) create mode 100644 demos/gnoBigBssProbe.c create mode 100644 demos/gnoComposite.c create mode 100644 demos/gnoEnvProbe.c create mode 100644 demos/gnoErrnoProbe.c create mode 100644 demos/gnoExecDiag.c create mode 100644 demos/gnoExecProbe.c create mode 100644 demos/gnoExeced.c create mode 100644 demos/gnoFdProbe.c create mode 100644 demos/gnoForkWaitProbe.c create mode 100644 demos/gnoHeapProbe.c create mode 100644 demos/gnoIdProbe.c create mode 100644 demos/gnoSignalProbe.c create mode 100644 demos/gnoStatProbe.c create mode 100644 demos/gnoWaitpidTimesProbe.c create mode 100644 runtime/include/fcntl.h create mode 100644 runtime/include/sys/stat.h create mode 100644 runtime/include/sys/times.h create mode 100644 runtime/include/sys/types.h create mode 100644 runtime/include/sys/wait.h create mode 100644 runtime/include/unistd.h diff --git a/demos/buildGno.sh b/demos/buildGno.sh index 9752e3d..17eeda7 100755 --- a/demos/buildGno.sh +++ b/demos/buildGno.sh @@ -103,7 +103,7 @@ fi "$RT/libcxxabi.o" "$RT/libcxxabiSjlj.o" "$RT/libunwindStub.o" echo "OMF: -> $OUTBASE.omf" -# Declare a dedicated DP/Stack (OMF KIND=0x1012) segment. Without it GNO +# Declare a dedicated DP/Stack (OMF KIND=0x4012) segment. Without it GNO # falls back to a 4 KB default stack shared with DP and placed low in bank 0, # which is too small / mis-placed for GS/OS file I/O: GNO's GS/OS interceptor # (StackGSOS) carves its direct-page work area off the caller's S and the FST @@ -111,10 +111,20 @@ echo "OMF: -> $OUTBASE.omf" # A sized $12 segment makes the Loader allocate a larger bank-0 block (S set # high in it), giving headroom and moving stack buffers off the collision. # This is the idiomatic ORCA/GNO mechanism (== #pragma stacksize). +# +# Size ceiling for exec-into: KERNexecve loads the NEW program via GS/OS +# InitialLoad2 BEFORE freeing the outgoing image (kern/gno/sys.c:762 vs 912), +# so during an execve() BOTH the caller's and the callee's DP/Stack segments +# occupy bank 0 at once. Bank 0 has ~48 KB usable for stacks, so a 16 KB +# default (== 32 KB transient across an exec) overflows and InitialLoad2 fails +# with a Memory Manager error that GNO maps to errno EIO -- exec-into never +# transfers control. 8 KB (16 KB transient) fits with margin and still gives +# ample GS/OS file-I/O headroom. Verified: 8 KB and 4 KB exec cleanly, 16 KB +# does not (demos/gnoExecProbe -> gnoExeced under real GNO/MAME). "$OMF" --input "$BIN" --map "$MAP" \ --base 0x1000 --entry __start --output "$OUT" \ --name "$(echo "$OUTBASE" | tr '[:lower:]' '[:upper:]' | cut -c1-8)" \ - --expressload --relocs "$RELOC" --stack-size "${GNO_STACK_SIZE:-0x4000}" + --expressload --relocs "$RELOC" --stack-size "${GNO_STACK_SIZE:-0x2000}" ls -la "$OUT" if [ "$DEBUG" = 1 ]; then diff --git a/demos/gnoBigBssProbe.c b/demos/gnoBigBssProbe.c new file mode 100644 index 0000000..6723c95 --- /dev/null +++ b/demos/gnoBigBssProbe.c @@ -0,0 +1,29 @@ +// D1 regression for the crt0Gno BSS-clear heartbeat bug: a large zero-init +// BSS array pushes __bss_end past the old ~0xB100 corruption threshold. +// crt0Gno no longer clears BSS (the loader fills the embedded zeros), so +// the program must run to the end AND read the array back as all-zero. +#include + +// ~5.75 KB. With the current GNO libc the runtime BSS base already sits at +// ~0xD000 (well above the old ~0xB100 heartbeat threshold), so this array's job +// is to keep __bss_end large (near the bank-0 top) while still fitting bank 0. +static volatile uint8_t bigBss[0x1700]; + +int main(void) { + *(volatile uint16_t *)0x025010UL = 0xBEEF; + uint16_t zeroed = 1; + unsigned i; + for (i = 0; i < 0x1700; i++) { + if (bigBss[i] != 0) { + zeroed = 0; + break; + } + } + *(volatile uint16_t *)0x025012UL = zeroed ? 0x0001 : 0xDEAD; + bigBss[0] = 1; // retain + bigBss[0x16FF] = 2; + *(volatile uint16_t *)0x025000UL = 0xC0DE; + for (volatile unsigned long j = 0; j < 600000UL; j++) { + } + return 0; +} diff --git a/demos/gnoComposite.c b/demos/gnoComposite.c new file mode 100644 index 0000000..0b265c6 --- /dev/null +++ b/demos/gnoComposite.c @@ -0,0 +1,199 @@ +// Composite "real program" regression for GNO/ME: a SINGLE process that chains +// every verified POSIX subsystem in one run, in an order chosen to stress the +// interactions the isolated probes never exercise -- fork AFTER file I/O (does a +// thread-spawn disturb the parent's FST/heap state?), signals AFTER fork (does +// SIGALRM delivery survive a fork/wait cycle?), and exec AFTER all of it (does +// image-replacement still work once the process has opened files, installed a +// handler, and touched the heap?). +// +// A stage counter at 0x025002 is bumped at the START of each stage, so a hang or +// crash pinpoints the offending subsystem in a single MAME run (instrument +// before theorizing). Run with GNO_PROG2=demos/gnoExeced.omf so the final +// execvp actually transfers control. +// +// Bank-2 absolute uint16 markers (DBR-independent), gnoHeapProbe convention: +// 0x025000 C0DE reached end (set by gnoExeced after exec, else by fallback) +// 0x025002 0007 stage counter: last stage reached (1..7); 7 == pre-exec +// 0x025010 E0EC exec transferred control to gnoExeced (set by the 2nd stage) +// 0x025020 0001 env: setenv/getenv("COMPOSITE") round-trip +// 0x025022 0001 file+stat: write/read/verify + stat st_size==16 + lseek +// 0x025024 0001 id/stat: fstat(1) char dev + isatty(1) + stat(".") dir +// 0x025026 0001 fork+waitpid: leaf child _exit(42), WEXITSTATUS==42, pid match +// 0x025028 0001 signals: SIGALRM via __sigTrampoline fired, pause() resumed +// 0x02502A 0001 times(): ret==0 && tms_stime==0 && tms_cstime==0 +// 0x02502E C41D fork child was scheduled + ran (set by the child) +#include +#include +#include +#include +#include +#include +#include +#include + +extern __attribute__((noreturn)) void _exit(int status); + +#define M(a) (*(volatile uint16_t *)(a)) +#define OK 0x0001 +#define BAD 0xDEAD + +static const char kPath[] = "composite.tmp"; +static const uint8_t kData[16] = { + 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, + 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50 +}; + +static volatile uint16_t rang; + + +// GNO fork child: a LEAF using only absolute-long stores + _exit (fresh DP). +static void child(void) { + M(0x02502EUL) = 0xC41D; + _exit(42); +} + + +static void onAlarm(int sig) { + (void)sig; + rang = 1; + M(0x025028UL) = OK; // handler executed via the trampoline +} + + +int main(void) { + struct stat sb; + struct tms tb; + uint8_t rbuf[16]; + char *ev; + char *cargv[2]; + long n; + long pos; + char one; + int fd; + int pid; + int st; + int w; + uint8_t i; + uint16_t ok; + + M(0x025000UL) = 0x0000; + M(0x025010UL) = BAD; + M(0x025020UL) = BAD; + M(0x025022UL) = BAD; + M(0x025024UL) = BAD; + M(0x025026UL) = BAD; + M(0x025028UL) = BAD; + M(0x02502AUL) = BAD; + M(0x02502EUL) = BAD; + + // Stage 1: environment round-trip (GS/OS shell-variable table). + M(0x025002UL) = 1; + if (setenv("COMPOSITE", "yes", 1) == 0) { + ev = getenv("COMPOSITE"); + if (ev != 0 && ev[0] == 'y' && ev[1] == 'e' && ev[2] == 's' && ev[3] == 0) { + M(0x025020UL) = OK; + } + } + + // Stage 2: file I/O + stat on the SAME file (compose fd layer + stat). + M(0x025002UL) = 2; + ok = OK; + fd = creat(kPath, 0); + if (fd >= 4) { + n = write(fd, kData, 16); + if (n != 16) { + ok = BAD; + } + close(fd); + } else { + ok = BAD; + } + if (stat(kPath, &sb) != 0 || sb.st_size != 16) { + ok = BAD; // stat on a real file we just wrote + } + fd = open(kPath, O_RDONLY); + if (fd >= 4) { + n = read(fd, rbuf, 16); + if (n != 16) { + ok = BAD; + } + for (i = 0; i < 16; i++) { + if (rbuf[i] != kData[i]) { + ok = BAD; + } + } + pos = lseek(fd, 4, SEEK_SET); + one = 0; + read(fd, &one, 1); + if (pos != 4 || (uint8_t)one != kData[4]) { + ok = BAD; + } + close(fd); + } else { + ok = BAD; + } + if (unlink(kPath) != 0) { + ok = BAD; + } + M(0x025022UL) = ok; + + // Stage 3: fstat/isatty on the console + stat(".") + credential calls. + M(0x025002UL) = 3; + ok = OK; + if (fstat(1, &sb) != 0 || !S_ISCHR(sb.st_mode)) { + ok = BAD; + } + if (isatty(1) != 1) { + ok = BAD; + } + if (stat(".", &sb) != 0 || !S_ISDIR(sb.st_mode)) { + ok = BAD; + } + (void)getuid(); // must not crash mid-composition + (void)getgid(); + M(0x025024UL) = ok; + + // Stage 4: times() sanity (deterministic zero-fill fields). + M(0x025002UL) = 4; + tb.tms_stime = 0xFFFFFFFFUL; + tb.tms_cstime = 0xFFFFFFFFUL; + if (times(&tb) == 0 && tb.tms_stime == 0 && tb.tms_cstime == 0) { + M(0x02502AUL) = OK; + } + + // Stage 5: fork a leaf child + reap with waitpid -- AFTER file I/O, to catch + // any FST/heap corruption a thread-spawn leaves in the parent. + M(0x025002UL) = 5; + pid = fork((void *)child); + if (pid > 0) { + st = 0; + w = waitpid(pid, &st, 0); + if (WIFEXITED(st) && WEXITSTATUS(st) == 42 && w == pid) { + M(0x025026UL) = OK; + } + } + + // Stage 6: signals -- AFTER fork, to prove SIGALRM delivery through the + // trampoline survives a fork/wait cycle and pause() resumes cleanly. + M(0x025002UL) = 6; + rang = 0; + signal(SIGALRM, onAlarm); + alarm(1); + pause(); + if (rang != 1) { + M(0x025028UL) = BAD; // handler did NOT run (overrides onAlarm) + } + + // Stage 7: exec into the 2nd program -- AFTER all the above. On success this + // never returns; gnoExeced sets 0x025010=E0EC and 0x025000=C0DE. + M(0x025002UL) = 7; + cargv[0] = "gnoexeced"; + cargv[1] = 0; + execvp("gnoexeced", cargv); + + // Reached only if the exec-into failed. + M(0x025000UL) = 0xC0DE; + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoEnvProbe.c b/demos/gnoEnvProbe.c new file mode 100644 index 0000000..25d915f --- /dev/null +++ b/demos/gnoEnvProbe.c @@ -0,0 +1,42 @@ +// Environment (GNO shell-variable table) round-trip regression. +// GNO has no Unix environ[]; the "environment" is the kernel SHELL- +// VARIABLE table reached via GS/OS Shell calls (SetGS $0146 / +// ReadVariableGS $014B) dispatched through the same inline $E100A8 +// mechanism as the file calls -- zero new asm. This probe sets a +// variable, reads it straight back, and asserts the value survives. +// +// Deterministic: the variable name/value are literals, no pids or +// addresses, so the round-trip is identical on every boot. +// +// Bank-2 absolute markers (DBR-independent), per gnoHeapProbe convention: +// 0x025000 C0DE reached end (no hang in the shell-call path) +// 0x025002 0001 getenv("LLVMTEST") == "42" (round-trip OK), else DEAD +// 0x025012 0001 setenv("LLVMTEST","42",1) == 0 (call succeeded), else DEAD +#include +#include + +#define M(a) (*(volatile uint16_t *)(a)) + + +int main(void) { + int sr; + char *v; + uint16_t match; + + M(0x025000UL) = 0x0000; + + sr = setenv("LLVMTEST", "42", 1); + M(0x025012UL) = (sr == 0) ? 0x0001 : 0xDEAD; + + v = getenv("LLVMTEST"); + match = 0xDEAD; + if (v != 0 && v[0] == '4' && v[1] == '2' && v[2] == 0) { + match = 0x0001; + } + M(0x025002UL) = match; + + M(0x025000UL) = 0xC0DE; // reached end + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoErrnoProbe.c b/demos/gnoErrnoProbe.c new file mode 100644 index 0000000..a80b909 --- /dev/null +++ b/demos/gnoErrnoProbe.c @@ -0,0 +1,93 @@ +// errno-space consistency probe for GNO/ME. The composite happy-path test +// proves the subsystems compose; this one hunts the KNOWN latent hazard: the +// fd layer maps GS/OS errors to POSIX errno via _mapErr, but the stat/exec/ +// access wrappers stash the RAW kernel errno. A real program that branches on +// `errno == ENOENT` breaks wherever the raw value leaks through. We trigger the +// SAME logical error (a missing file) through four wrappers and record the raw +// errno each sets, so the divergence (if any) is visible against ENOENT. +// +// Bank-2 absolute uint16 markers (DBR-independent): +// 0x025000 C0DE reached end +// 0x025030 ENOENT (from errno.h) -- the value a POSIX app expects +// 0x025032 errno after open("nosuch", O_RDONLY) [fd layer, _mapErr] +// 0x025034 errno after stat("nosuch") [stat wrapper, raw kernel] +// 0x025036 errno after unlink("nosuch") [fd layer, _mapErr] +// 0x025038 errno after access("nosuch", F_OK) [access over stat] +// 0x02503A 0001 if all four calls actually FAILED (rc==-1), else which passed +// 0x02503C 0001 if every failing errno == ENOENT (POSIX-consistent), else DEAD +#include +#include +#include +#include +#include + +#define M(a) (*(volatile uint16_t *)(a)) +#define OK 0x0001 +#define BAD 0xDEAD + +static const char kMissing[] = "nosuchfile.xyz"; + + +int main(void) { + struct stat sb; + uint16_t failed; + uint16_t consistent; + int eOpen; + int eStat; + int eUnlink; + int eAccess; + int rc; + int fd; + + M(0x025000UL) = 0x0000; + + M(0x025030UL) = (uint16_t)ENOENT; // what a POSIX app checks against + + failed = OK; + + errno = 0; + fd = open(kMissing, O_RDONLY); + eOpen = errno; + if (fd != -1) { + failed = BAD; // unexpectedly succeeded + close(fd); + } + M(0x025032UL) = (uint16_t)eOpen; + + errno = 0; + rc = stat(kMissing, &sb); + eStat = errno; + if (rc != -1) { + failed = BAD; + } + M(0x025034UL) = (uint16_t)eStat; + + errno = 0; + rc = unlink(kMissing); + eUnlink = errno; + if (rc != -1) { + failed = BAD; + } + M(0x025036UL) = (uint16_t)eUnlink; + + errno = 0; + rc = access(kMissing, F_OK); + eAccess = errno; + if (rc != -1) { + failed = BAD; + } + M(0x025038UL) = (uint16_t)eAccess; + + M(0x02503AUL) = failed; + + consistent = OK; + if (eOpen != ENOENT || eStat != ENOENT || eUnlink != ENOENT || eAccess != ENOENT) { + consistent = BAD; + } + M(0x02503CUL) = consistent; + + M(0x025000UL) = 0xC0DE; + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoExecDiag.c b/demos/gnoExecDiag.c new file mode 100644 index 0000000..f061fe0 --- /dev/null +++ b/demos/gnoExecDiag.c @@ -0,0 +1,65 @@ +// Diagnostic for the gnoExecProbe exec-into failure: distinguishes a PATH/ +// access() resolution miss from a K_execve transfer-of-control failure. +// Runs in the SAME harness as gnoExecProbe (GNO_PROG2=gnoExeced deployed to +// /bin). Each marker isolates one step so we can see exactly where exec dies. +// +// Bank-2 absolute uint16 markers (DBR-independent): +// 0x025000 C0DE reached end (fallback; set only if every exec returned) +// 0x025010 E0EC gnoExeced ran -> some exec transferred control (success) +// 0x025020 F00D access("/bin/gnoexeced", F_OK) == 0 (our libc finds it) +// else EE00|errno (access failed -> which errno) +// 0x025022 EE00|errno after execv("/bin/gnoexeced") RETURNED (explicit path, +// bypasses PATH; only set if the exec failed to transfer) +// 0x025024 EE00|errno after execvp("gnoexeced") RETURNED (PATH-resolved; +// only set if that exec failed too) +#include + +extern int access(const char *path, int mode); +extern int execv(const char *path, char *const *argv); +extern int execvp(const char *file, char *const *argv); +extern int errno; + +#define M(a) (*(volatile uint16_t *)(a)) +#define BAD 0xDEAD +#define F_OK 0 + + +int main(void) { + char *argv[2]; + int r; + + M(0x025000UL) = 0x0000; + M(0x025010UL) = BAD; + M(0x025020UL) = BAD; + M(0x025022UL) = BAD; + M(0x025024UL) = BAD; + + argv[0] = "gnoexeced"; + argv[1] = 0; + + // Step 1: does our own access() see the file gsh clearly found at /bin? + errno = 0; + r = access("/bin/gnoexeced", F_OK); + if (r == 0) { + M(0x025020UL) = 0xF00D; + } else { + M(0x025020UL) = 0xEE00 | (uint16_t)(errno & 0xFF); + } + + // Step 2: explicit-path execv (bypasses $PATH entirely). On success this + // never returns -- gnoExeced sets 0x025010 + 0x025000 in the same image. + errno = 0; + execv("/bin/gnoexeced", argv); + M(0x025022UL) = 0xEE00 | (uint16_t)(errno & 0xFF); // only if it returned + + // Step 3: PATH-resolved execvp, as gnoExecProbe does. + errno = 0; + execvp("gnoexeced", argv); + M(0x025024UL) = 0xEE00 | (uint16_t)(errno & 0xFF); // only if it returned + + // Reached only if BOTH execs failed to transfer control. + M(0x025000UL) = 0xC0DE; + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoExecProbe.c b/demos/gnoExecProbe.c new file mode 100644 index 0000000..96ced88 --- /dev/null +++ b/demos/gnoExecProbe.c @@ -0,0 +1,81 @@ +// exec-family regression for the GNO libc (punch-list #5 + #7). +// Exercises access() over Kstat and the exec* wrappers over K_execve. +// +// Two halves, run in order so one boot can cover both: +// 1. Negative / no-2nd-program tests (always run, fully deterministic): +// - access(".", F_OK) succeeds on the login cwd (a directory). +// - access("/nonesuch/no", F_OK) fails (-1) on a missing path. +// - execvp("nosuchprog", ...) fails (-1, errno set): buildPath's +// $PATH search finds no candidate and returns before ever reaching +// the kernel, so this validates buildCmd/buildPath/access without +// needing a second executable. +// 2. exec-into (needs GNO_PROG2=demos/gnoExeced.omf placed on /bin): the +// LAST action execvp's the tiny 2nd program "gnoexeced", which sets +// its own bank-2 marker and the reached-end word. If GNO_PROG2 is +// not supplied the exec-into fails, prog1 sets the fallback reached- +// end word, and only the half-1 checks are meaningful. +// +// Bank-2 absolute uint16 markers (DBR-independent), gnoHeapProbe convention: +// 0x025000 C0DE reached end (set by gnoExeced on success, else by prog1) +// 0x025002 0001 execvp("nosuchprog") returned -1, else DEAD +// 0x025004 0001 errno was set nonzero by the failed execvp, else DEAD +// 0x025008 0001 access(".", F_OK) == 0 (existing dir), else DEAD +// 0x02500A 0001 access("/nonesuch/no", F_OK) == -1 (missing), else DEAD +// 0x025010 E0EC gnoExeced ran (exec-into transferred control), else DEAD +#include + +extern int access(const char *path, int mode); +extern int execvp(const char *file, char *const *argv); +extern int errno; + +#define M(a) (*(volatile uint16_t *)(a)) +#define OK 0x0001 +#define BAD 0xDEAD +#define F_OK 0 + + +int main(void) { + char *nope[2]; + char *prog2[2]; + int r; + + M(0x025000UL) = 0x0000; + M(0x025002UL) = BAD; + M(0x025004UL) = BAD; + M(0x025008UL) = BAD; + M(0x02500AUL) = BAD; + M(0x025010UL) = BAD; + + // access() over Kstat: existing directory vs. missing path. + if (access(".", F_OK) == 0) { + M(0x025008UL) = OK; + } + if (access("/nonesuch/no", F_OK) == -1) { + M(0x02500AUL) = OK; + } + + // Failed exec: no such program on $PATH -> -1 with errno set. (This + // fails in buildPath, before K_execve, so it is safe and deterministic.) + nope[0] = "nosuchprog"; + nope[1] = 0; + errno = 0; + r = execvp("nosuchprog", nope); + if (r == -1) { + M(0x025002UL) = OK; + } + if (errno != 0) { + M(0x025004UL) = OK; + } + + // exec-into the 2nd program (GNO_PROG2). On success this never returns; + // gnoExeced sets 0x025010 and the reached-end word in the same image. + prog2[0] = "gnoexeced"; + prog2[1] = 0; + execvp("gnoexeced", prog2); + + // Only reached if the exec-into failed (no GNO_PROG2 on /bin). + M(0x025000UL) = 0xC0DE; + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoExeced.c b/demos/gnoExeced.c new file mode 100644 index 0000000..51a757c --- /dev/null +++ b/demos/gnoExeced.c @@ -0,0 +1,21 @@ +// 2nd program for gnoExecProbe (punch-list #5 + #7 exec-into test). +// Proves exec* transferred control: GNO exec replaces the process image in +// place, so this runs in the SAME process after gnoExecProbe's execvp. It +// sets its exec-into marker plus the shared reached-end word, then spins so +// the harness can sample bank-2 memory. +// +// Deployed to /bin as "gnoexeced" via GNO_PROG2 (runInGno.sh copies it to +// /GNO.BOOT/bin), which is how gnoExecProbe's execvp("gnoexeced") resolves +// it through the default $PATH ("/bin /usr/bin"). +#include + +#define M(a) (*(volatile uint16_t *)(a)) + + +int main(void) { + M(0x025010UL) = 0xE0EC; // exec-into transferred control here + M(0x025000UL) = 0xC0DE; // reached end (via exec'd program) + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoFdProbe.c b/demos/gnoFdProbe.c new file mode 100644 index 0000000..9d21eae --- /dev/null +++ b/demos/gnoFdProbe.c @@ -0,0 +1,103 @@ +// POSIX fd-layer regression for GNO (punch-list #9): exercise the GS/OS-toolbox +// emulation of creat/write/close/open/read/lseek/unlink over the gsos* wrappers. +// Round-trips a fixed 16-byte payload through a real GS/OS file in the login cwd +// (/home/root) and asserts every stage via bank-2 absolute markers (DBR- +// independent), per the gnoHeapProbe/gnoForkWaitProbe convention. +// +// 0x025000 C0DE reached end (no hang in the fd layer) +// 0x025002 0001 read-back payload matched what was written +// 0x025004 0001 creat() returned a real GS/OS refNum (>= 4) +// 0x025006 0001 write() wrote all 16 bytes +// 0x025008 0001 read() read all 16 bytes back +// 0x02500A 0001 open(O_RDONLY) returned a real refNum (>= 4) +// 0x02500C 0001 lseek(SEEK_SET,4) + 1-byte read landed on payload[4] +// 0x02500E 0001 unlink() removed the file +#include +#include +#include + +#define M(a) (*(volatile uint16_t *)(a)) + +static const char kPath[] = "fdprobe.tmp"; +static const uint8_t kData[16] = { + 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, + 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50 +}; + + +int main(void) { + uint8_t rbuf[16]; + uint16_t ok; + int fd; + long n; + long pos; + char one; + uint8_t i; + + M(0x025000UL) = 0x0000; + M(0x025002UL) = 0xDEAD; + M(0x025004UL) = 0xDEAD; + M(0x025006UL) = 0xDEAD; + M(0x025008UL) = 0xDEAD; + M(0x02500AUL) = 0xDEAD; + M(0x02500CUL) = 0xDEAD; + M(0x02500EUL) = 0xDEAD; + ok = 1; + + // Create + write the payload. + fd = creat(kPath, 0); + if (fd >= 4) { + M(0x025004UL) = 0x0001; + n = write(fd, kData, 16); + if (n == 16) { + M(0x025006UL) = 0x0001; + } else { + ok = 0; + } + close(fd); + } else { + ok = 0; + } + + // Reopen read-only and read the payload back. + fd = open(kPath, O_RDONLY); + if (fd >= 4) { + M(0x02500AUL) = 0x0001; + n = read(fd, rbuf, 16); + if (n == 16) { + M(0x025008UL) = 0x0001; + } else { + ok = 0; + } + for (i = 0; i < 16; i++) { + if (rbuf[i] != kData[i]) { + ok = 0; + } + } + // Seek back into the file and confirm the byte at offset 4. + pos = lseek(fd, 4, SEEK_SET); + one = 0; + read(fd, &one, 1); + if (pos == 4 && (uint8_t)one == kData[4]) { + M(0x02500CUL) = 0x0001; + } else { + ok = 0; + } + close(fd); + } else { + ok = 0; + } + + // Remove the file. + if (unlink(kPath) == 0) { + M(0x02500EUL) = 0x0001; + } else { + ok = 0; + } + + M(0x025002UL) = ok ? 0x0001 : 0xDEAD; + M(0x025000UL) = 0xC0DE; + for (volatile unsigned long j = 0; j < 600000UL; j++) { + } + return 0; +} diff --git a/demos/gnoForkWaitProbe.c b/demos/gnoForkWaitProbe.c new file mode 100644 index 0000000..57a5d44 --- /dev/null +++ b/demos/gnoForkWaitProbe.c @@ -0,0 +1,56 @@ +// Process-lifecycle core regression for GNO: fork a leaf child that _exit(42)s, +// have the parent wait() and assert the real exit status propagates. Covers two +// fixed bugs at once: +// 1. Kernel dispatch vector: gnoKernel.s called $E10000 (the standard Tool +// Locator, toolset $03 = Misc Tools) instead of GNO's udispatch $E10008, so +// EVERY K* primitive hung. Latent until this probe -- fork is the first K* +// call ever exercised under GNO. +// 2. Exit status: _exit routed QUIT through __gnoGsosCall, which loaded +// A = callNum (0x2029) right before the trap, so GNO's StackGSOS recorded +// 0x2029 as the exit code and wait() decoded WEXITSTATUS = 0x29 (41). +// __gnoQuit now leaves A = status, so wait() sees 42 (0x2A). +// +// GNO fork is a thread-spawn: the child begins at child() on a fresh stack/DP, so +// child() is a LEAF using only absolute-long stores + _exit (no DP scratch). +// Bank-2 absolute markers (DBR-independent), per the gnoHeapProbe convention. +// 0x025000 C0DE reached end (no hang in fork/wait) +// 0x025010 C41D child was scheduled + ran +// 0x025014 002A WEXITSTATUS == 42 (the status propagated) +// 0x025016 0001 wait() reaped the same pid fork() returned +// 0x025018 600D exit status is NOT the old 41/0x29 miscompile +#include +#include + +extern int fork(void *subr); // GNO thread-spawn: subr = (void *)&childFn +extern int wait(int *status); +extern __attribute__((noreturn)) void _exit(int status); + +#define M(a) (*(volatile uint16_t *)(a)) + + +static void child(void) { + M(0x025010UL) = 0xC41D; // child was scheduled + entered child() + _exit(42); // exit code 42 -> status word 0x2A00 +} + + +int main(void) { + M(0x025000UL) = 0x0000; + int pid = fork((void *)child); + if (pid <= 0) { + M(0x025016UL) = 0xDEAD; // fork failed / returned no child + M(0x025000UL) = 0xC0DE; + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; + } + int st = 0; + int w = wait(&st); // blocks until the child dies (race-free) + M(0x025014UL) = WIFEXITED(st) ? (uint16_t)WEXITSTATUS(st) : 0xEEEE; // == 0x002A + M(0x025016UL) = (w == pid) ? 0x0001 : 0xDEAD; // pid round-trip + M(0x025018UL) = (WEXITSTATUS(st) == 41) ? 0xDEAD : 0x600D; // guard old bug + M(0x025000UL) = 0xC0DE; // reached end + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoHeapProbe.c b/demos/gnoHeapProbe.c new file mode 100644 index 0000000..af83381 --- /dev/null +++ b/demos/gnoHeapProbe.c @@ -0,0 +1,41 @@ +// A1 regression: pre-fix, malloc bumped the bank-0 phantom heap window +// [__bss_end, 0xBF00) into GNO's kernel heartbeat block (alarmHB ~0xB7xx) +// -> SysFailMgr $0308 halt. crt0Gno now backs malloc with an MM-owned +// handle (__heapInitMM), so several KB of allocations survive intact. +#include +#include +#include + +int main(void) { + *(volatile uint16_t *)0x025010UL = 0xBEEF; // main entered + uint16_t ok = 1; + char *b[8]; + uint8_t i; + for (i = 0; i < 8; i++) { + b[i] = (char *)malloc(0x400); // 8 KB total + if (!b[i]) { + ok = 0; + break; + } + memset(b[i], (char)(0xA0 + i), 0x400); + } + if (ok) { + for (i = 0; i < 8; i++) { + if ((uint8_t)b[i][0] != (uint8_t)(0xA0 + i) || + (uint8_t)b[i][0x3FF] != (uint8_t)(0xA0 + i)) { + ok = 0; + break; + } + } + } + // The observable regression signal is survival: pre-fix, malloc bumped + // into GNO's kernel heartbeat block and 8 KB of allocations could not + // complete intact. (A bank read cannot prove MM-ownership -- NewHandle + // uses attr without attrBank, so the MM may legitimately place the block + // in bank 0 -- so this probe asserts survival, not the block's bank.) + *(volatile uint16_t *)0x025012UL = ok ? 0x0001 : 0xDEAD; + *(volatile uint16_t *)0x025000UL = 0xC0DE; // reached end + for (volatile unsigned long j = 0; j < 600000UL; j++) { + } + return 0; +} diff --git a/demos/gnoIdProbe.c b/demos/gnoIdProbe.c new file mode 100644 index 0000000..1da82ba --- /dev/null +++ b/demos/gnoIdProbe.c @@ -0,0 +1,47 @@ +// uid/gid + job-control regression for GNO (punch-list #11). Exercises the +// non-destructive getter surface -- getuid/geteuid/getgid/getegid, getpid, and +// getpgrp -- and asserts the boot-independent invariants: real == effective +// ids, a valid (non-error) process group, and a positive pid. The MUTATING +// calls (setuid/setgid/setpgid/tcsetpgrp) are deliberately NOT driven here: +// they would alter this process' credentials / process group under gsh and make +// the run non-deterministic. tcgetpgrp/setsid are ENOSYS stubs and need no run. +// +// Bank-2 absolute markers (DBR-independent), per the gnoHeapProbe convention. +// 0x025000 C0DE reached end +// 0x025010 0001 getuid() == geteuid() (real/effective uid consistent) +// 0x025012 0001 getgid() == getegid() (real/effective gid consistent) +// 0x025014 0001 getpgrp() >= 0 (K_getpgrp did not fail) +// 0x025016 0001 getpid() > 0 (valid pid) +#include +#include + +#define M(a) (*(volatile uint16_t *)(a)) + + +int main(void) { + int uid; + int euid; + int gid; + int egid; + int pid; + int pgrp; + + M(0x025000UL) = 0x0000; + uid = (int)getuid(); + euid = (int)geteuid(); + gid = (int)getgid(); + egid = (int)getegid(); + pid = getpid(); + pgrp = (int)getpgrp(); + + M(0x025010UL) = (uid == euid) ? 0x0001 : 0xDEAD; + M(0x025012UL) = (gid == egid) ? 0x0001 : 0xDEAD; + M(0x025014UL) = (pgrp >= 0) ? 0x0001 : 0xDEAD; + M(0x025016UL) = (pid > 0) ? 0x0001 : 0xDEAD; + M(0x025018UL) = (uint16_t)pgrp; // informational + M(0x02501AUL) = (uint16_t)uid; // informational + M(0x025000UL) = 0xC0DE; // reached end + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoSignalProbe.c b/demos/gnoSignalProbe.c new file mode 100644 index 0000000..429b0a4 --- /dev/null +++ b/demos/gnoSignalProbe.c @@ -0,0 +1,43 @@ +// Signals end-to-end regression for GNO: install a SIGALRM handler via our +// signal() (which routes the kernel callback through __sigTrampoline), arm a +// 1-second alarm, and pause(). The kernel delivers SIGALRM through the +// trampoline, which adapts the ORCA/C callee-pops frame to our arg0-in-A +// handler; the handler flips a flag + drops a marker, pause() returns (EINTR), +// and main verifies the handler ran and control resumed cleanly (proving the +// trampoline's ORCA callee-pop return kept the kernel's context-restore stack +// intact). +// +// Bank-2 absolute uint16 markers (DBR-independent), gnoHeapProbe convention: +// 0x025000 C0DE reached end (pause returned; no hang/crash) +// 0x025010 C41D the SIGALRM handler actually executed +// 0x025012 0001 handler-ran flag observed after pause (0xDEAD if not) +#include +#include +#include + +#define M(a) (*(volatile uint16_t *)(a)) + +static volatile uint16_t rang; + + +static void onAlarm(int sig) { + (void)sig; + rang = 1; + M(0x025010UL) = 0xC41D; // handler executed via the trampoline +} + + +int main(void) { + M(0x025000UL) = 0x0000; + M(0x025010UL) = 0x0000; + M(0x025012UL) = 0x0000; + rang = 0; + signal(SIGALRM, onAlarm); // install through __sigTrampoline + alarm(1); // fire SIGALRM in ~1 second + pause(); // block until the signal is delivered + M(0x025012UL) = rang ? 0x0001 : 0xDEAD; + M(0x025000UL) = 0xC0DE; // reached end (returned cleanly) + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/gnoStatProbe.c b/demos/gnoStatProbe.c new file mode 100644 index 0000000..daab1f4 --- /dev/null +++ b/demos/gnoStatProbe.c @@ -0,0 +1,72 @@ +// stat/fstat/isatty regression for the GNO libc file-status surface. +// +// Exercises the three K* status primitives (Kstat/Kfstat 0x2703/0x2803) +// through their POSIX wrappers in libcGno.c, plus isatty() built on +// fstat. The hard part these wrappers get right is struct stat's +// byte-layout: the GNO kernel (kern/gno/stat.c, ORCA/C) writes the buffer +// directly, so if runtime/include/sys/stat.h drifts by even one field the +// S_IF* type bits land in the wrong word and every test below flips. +// +// The program is launched at the gsh prompt with stdout on the console +// TTY, so: +// - fstat(1) succeeds and st_mode is a character special (the TTY). +// - isatty(1) is therefore true. +// - stat(".") on the login cwd is a directory. +// All deterministic (no pids / addresses / times inspected). +// +// Bank-2 absolute uint16 markers (DBR-independent), per the gnoHeapProbe +// convention: +// 0x025000 C0DE reached end (no hang in any stat path) +// 0x025010 0001 fstat(1) returned 0 +// 0x025012 0001 S_ISCHR(fstat st_mode) -- stdout is a char device +// 0x025014 0001 isatty(1) == 1 +// 0x025016 0001 stat(".") returned 0 +// 0x025018 0001 S_ISDIR(stat st_mode) -- cwd is a directory +#include +#include + +extern int isatty(int fd); + +#define M(a) (*(volatile uint16_t *)(a)) +#define OK 0x0001 +#define BAD 0xDEAD + + +int main(void) { + struct stat sb; + int rc; + + M(0x025010UL) = BAD; + M(0x025012UL) = BAD; + M(0x025014UL) = BAD; + M(0x025016UL) = BAD; + M(0x025018UL) = BAD; + + // fstat(1): stdout, which the harness leaves on the console TTY. + rc = fstat(1, &sb); + if (rc == 0) { + M(0x025010UL) = OK; + } + if (S_ISCHR(sb.st_mode)) { + M(0x025012UL) = OK; + } + + // isatty(1): true because stdout is a character device. + if (isatty(1) == 1) { + M(0x025014UL) = OK; + } + + // stat("."): the login cwd, which is a directory. + rc = stat(".", &sb); + if (rc == 0) { + M(0x025016UL) = OK; + } + if (S_ISDIR(sb.st_mode)) { + M(0x025018UL) = OK; + } + + M(0x025000UL) = 0xC0DE; // reached end + for (volatile unsigned long j = 0; j < 600000UL; j++) { + } + return 0; +} diff --git a/demos/gnoWaitpidTimesProbe.c b/demos/gnoWaitpidTimesProbe.c new file mode 100644 index 0000000..a9a2d2a --- /dev/null +++ b/demos/gnoWaitpidTimesProbe.c @@ -0,0 +1,59 @@ +// waitpid + times regression for GNO/ME. Forks a leaf child that _exit(37)s, +// then the parent reaps it with waitpid(pid, &st, 0) -- verifying the pure-C +// waitpid loop returns the exact child pid and the real exit status -- and +// finally calls times() and checks the deterministic fields the GNO kernel +// zero-fills (return value, tms_stime, tms_cstime), a crash-free sanity marker. +// +// GNO fork is a thread-spawn: the child begins at child() on a fresh stack/DP, +// so child() is a LEAF using only absolute-long stores + _exit (no DP scratch). +// Bank-2 absolute markers (DBR-independent), per the gnoForkWaitProbe convention. +// 0x025000 C0DE reached end (no hang in waitpid/times) +// 0x025010 C41D child was scheduled + ran +// 0x025014 0025 WEXITSTATUS == 37 (status propagated through waitpid) +// 0x025016 0001 waitpid reaped the same pid fork() returned +// 0x025018 0001 times() sanity: ret==0 && tms_stime==0 && tms_cstime==0 +#include +#include +#include + +extern int fork(void *subr); // GNO thread-spawn: subr = (void *)&childFn +extern __attribute__((noreturn)) void _exit(int status); +extern int waitpid(int pid, int *status, int options); +extern clock_t times(struct tms *buf); + +#define M(a) (*(volatile uint16_t *)(a)) + + +static void child(void) { + M(0x025010UL) = 0xC41D; // child was scheduled + entered child() + _exit(37); // exit code 37 -> status word 0x2500 +} + + +int main(void) { + M(0x025000UL) = 0x0000; + int pid = fork((void *)child); + if (pid <= 0) { + M(0x025016UL) = 0xDEAD; // fork failed / returned no child + M(0x025000UL) = 0xC0DE; + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; + } + int st = 0; + int w = waitpid(pid, &st, 0); // pure-C loop over wait() until pid reaped + M(0x025014UL) = WIFEXITED(st) ? (uint16_t)WEXITSTATUS(st) : 0xEEEE; // == 0x0025 + M(0x025016UL) = (w == pid) ? 0x0001 : 0xDEAD; // pid round-trip + + struct tms tb; + tb.tms_stime = 0xFFFFFFFFUL; // poison; kernel must overwrite with 0 + tb.tms_cstime = 0xFFFFFFFFUL; + clock_t rc = times(&tb); + uint16_t timesOk = (rc == 0 && tb.tms_stime == 0 && tb.tms_cstime == 0) ? 0x0001 : 0xDEAD; + M(0x025018UL) = timesOk; + + M(0x025000UL) = 0xC0DE; // reached end + for (volatile unsigned long j = 0; j < 300000UL; j++) { + } + return 0; +} diff --git a/demos/rsrcProbe.apl b/demos/rsrcProbe.apl index 2c569d4b9613ec6501d7d105eaf0a2170debbc6f..b9cb8d2e856a24f500a3d6bb0b67ade543424a11 100644 GIT binary patch delta 4756 zcmcJSe^8Xi8OQg1-#hNk^U9rwfG6QNLgAw59ZCQr0YcDIV;z$qA~A}*AV?FNS~F_K zp77>%BGp##LNw}FN=%H#U-Wr)IkD5} zf9;JApM9S1?moNE?jF*2g}-`*-<2N!IU$562$Y+Y9pcAL-E@p zt%Mwo$baf_UN0jzFT!W*g-*A#ayoQ!K2M0(d9zGe!&M}cRod~6_U~3Y=Zq(t*{6$; zVSejf=Z?inA2(GGnz2~1@U{Hjd}RY)&acZ+{>F#VJ0MKsCuJ)kVR}mFk-SUU#J=r} zpKq0W{G?Ux*Y^wyP1i{~N93@Qs!Op4jqT(_<{1Hg%H0}zgnC2{DwVqFnPDxQ-YtjQ zNs-*Ih0hq;GY(+`)^lIio_3-f*2Tx`TBR&lx|$_Nn&knQYImePM)^wT^aY{Cw@MkP zW>8h@*&PwXxR7ow)b*_oT8Cg2M6%GiIg%*{hxHJUgIX}n(gkvi928-VQo7bHq|@}U znaKj(3=Ya1#tEQ*hz-QG2j#F9n(TAk^~s?wJvpRr*AY3Sg^j?>GrSg(%>I~GIpo*1 z%3*!zjL?+EX~*U6cG!zW3(FBLBoDyBeGrj@ADedxO}``%>DI!MC9$5zWZm_UGo%Ga z-4JzE3ykcBkvxtJFVlke&jk5ts-xsTG8__`_Km1?ZpcsPhAqoM)tp#wByulo5EJy{ zrhBxY@>EQoPtR##%r2QVnw@s0ZV>wh?mMyr4=obZtlA8vhC{b&p=vuERX;4PM;=hkP;$63q*Y47E0e^oOgFnl2^dOXq44gA z4>kjb+;k4ANz1^oGW4n(&JUN9NGV!xbqZYEVBR2X>+T3_vj0D#^xtAD&ak zBZ>9!JhgB`r~4p0RX4G}E?MBMO0n=(C7Tcn*ZTyA%QwTblV-ph%^RE{VX37o)jXI5 zacJ65oZ3EeH*avNWWb0^wGFx;4ku<(ZCxWH(Ib;$RYTXv%68xA5lRLs)8_(RO=$)x zLyZ+Oq*yq#t?)inJ%rK0$D*2PLa+DxiCfh;uNv2(+MI_y^gBwjHN%N@tkUt zBk%;+^_TRjO-ffSQW||nQmml&4QH#S#OhcmpSfBOtAKKkj7WlNKp09eB$U8nT_Z$F zh3rD=@OagN`ObPs(>p|s?MSRPNwLkaJbks9?bV`kY$w=&E_s6%DHiLI0`*9NK2X(U zygSK0dU;e6yJf~!xPK6~vuG^4W%pe?RSzNfLb&hZ!L$&IX_ew}WIP?oV&ht+IE=-y z*=)Nb2jQUca7WL14zVj6jIl8=IKi(|nvJtKopQpMFcC83ey4+eRZouKst?hI`;ov0J5bd~>&|b#k zW%uHXS$sWn0M<&nb_)@pB z&_36eAWC+ByeMV(&7zd#H;GD{eXb!+lx%*RsQlBOZip4(E7u|__9=UD(1%k7`IYX# zzU1XRf6g7amD0xXpSqQOso(Io+{!D?wfx3qf$yESdA_7Pa5eor$8T~gUuJY*SkBDl z_;^p?xT~DULC0M#rDz%*=*n8nU-ATYX6@!Ix69#*NUct6;)o0D%LUOw^Z2`RVIun2dUB#RpQ2!PO5wo zRVI!qK1kF{rT(atG&bl5s@#YwiDQb@gUg=a8C~|IG36nuOp7WBW6I-HnHyDjRGwjb zeU?hAqLOZG&_=3kjw<8El;2b3)u<9Pro2OyKYEmp+=*G*fLDun)$v}{>QyJeVs%F{o?*Dq$BolWkq2k0DxIc`6fI=UDav0< ztjfmXmZ*~Z*r+m1ITUTOPOcJpY|Q>>|2H1L*MDLuHUm|q+c{GSy`wXnSCsGOKKA)HW>hIwy6Wdb(uRSqYg@x(O6vtT1@PEocm zv?^y8ygKH-#hRmF9k@mJb+86LUi5%Li9GE|xK;_j7$bRuI;(E}$jIbz9oj(Nookg! zW?8)_y(uvNq)6$*`#oMkLmAq@iw9Q98>G;EWex#`9m_ zoADx@(YN$+YSFR-m$i%ZLcpwFt13?;Jqy+V*0`GZ$S!ES z33dz&C;V!ZLqG%gb})?pf6Vv4pSS=P+gC%#EwH3oLWaN}{%>ZwCj>qapI+d*CJ-lC zYXHn9*UJF&Nab|EBJw-|JV|b1@O?68Jm4>6$6~+@Vk`!%;`ZhPj&kdA04?x`6W|2= z;RHxG9`*oUHJ(NEji0g<@Qlsx23RN1M1P0__yp1uPsKNqOHthVz)D1~;~e#zpCanh zZXv!%+qewy`AvqXWiGrvsj!ehGsH_iY?dluZ+Tp8dm2 z@FvV$Y`P}+$m=uLSiw!0Ma9Nh7$uu?BDkmBIW+k0qgQzF%!PYgfQJj$r2&3kNVUEq zI$(YAkvItMD5k-q#kA*@V%oFjF^UIEDE2Z;dYpQ`F2+|%smsl!*vz2!(v4cYH2rEX zo%BU7P582xF8iVR6b~$<<33^7_5}63iztQ|4*iUJX0d2LEY!}B>SIh7A8UhX7V~nL zH;-YF=vf8Y5-}f9CN4o#hz*Dp;!B7sQ9;y*2N3n*M~LUdvxpbOONb5P9mFQlS_Rl5 zdJv7GuMjDVOAy<|b=82K;tKRy#0`ko#74v(Q9fC z6kyf%*Ywp~6P_em%`G5luSu!}_-lqxJX|x3c#q*I!!d@F45t{n7)~>M22gIjxB6cQ C;1%}( delta 4224 zcmb`Je^iuJ8prSZz5~OLLxF~X2o9s{NWu&Vh7bf|kSOMH5ywd>=AB6)Yj^vhZF`PQ z^7?wpvXyf;Yfp7Trq|t}oifI9T}#w{oZOBlnw=&b5fGEqHP_tO%pb7NbMH*!>74#+ z^x<=#=X;;$KKH)&9k_H}JaJy!o|}4r5F!%t&NZM7ERm3Rn>xx{)As1VOVEKP5RwdI z{^{6f6mmKSvINp`rLgk-#u3BfR`r0h_pkx}6?VPk9Dd2UJ9W!&Cn0YStA7s}-)kgS z&cNrX(ZhwtmH7mI+6h@`>}{lv3AU^co3#Vop1sY+vIO!6{y9R(Me(U|sp1+@E~V|FRV*o{$HgFeJ}F;3ZlNb7^Yq~D^Uls8nOpk2v7KtjtLsz;V?r0D z_V0;DAgZ65b<;BgaURk;t51TTYQKgaVIENfv{h&RwO{k+_N#smsa6Lyf1loC+KmI~ z=Z2{vkFHa(K+|rEI=odK(FS3wFAjT1r&55?0v3hR5EJPcozWG5QEaDTLe<1`LJ)$s z0$T9Mw=Nj%hMtJp&sGx1EY*K81_P|2BOu4BerG`QE4f-g%Ee|>|18*CjD%HKgWzHq zDn?TSP#`$fl{fBEgGXY>?ii1bs6ovi2b|R>YC$E@D|V`WFX>c+G5$WOeTJYNQ2TK& zNm@`H*8J)S>~8O{8u%b_yVU*&fk;2(C8f8CxLAK1>oO2_{mgYIL1sT>ikKNRX@MIn zK}&3@edH%AP)lsu)PNdrC#JW>5p@8zgXIQsiyEjF3C0h|lF z4==@*4!IM-4Mg@Z*lrcH!Emb~oDY{E-I1sy!BR?=*6+wtgWd!@kho5T9qoWy`XC7! z*p7#*2rkY4Q4m~3+K$ghk+k4P;YT%~4a^XfOt&OuMxPHH=OAL045jg_5quF_4r=}FCnK8`_+|&( zDC(PhW2V!BxWP;Z^13nn^sS|WILYEWG}|g-(xKUTbb*;A=38X(O^a{41E2Hh;mCCo4XUgLdB_XQ(ktrP!B{r&fnDRzM znZ^}$c+1Flg#7rwOG+{lnq6E2cbUq?9pe5XI#yg6*Pti(;$_o=CAZPvl`IsUH_?BT zY{Xe*b6dp*J$29hUet=|E4O9hYq0OODe=M(?WOAqm9J;c)1hwa@e=A_d z#Qf)Gyx?58tIu6&d@BWSM%h-S)LkZ(!W*+m|J_XDE;YgeZ`RDlBFAfrut@Kn`Luk& zVm_Qk^>xYAx1b}UJaT7PdHlu+&RR9W0@5$xHL; zq$Pif3v-H=ar-2HnVJs1{!<4d#kG|O6;w$BD%jl=U!>5_Hz6BAPty0wvc*Z0|| z)7n;acDZ(LojU5)sb>r@KEKZJ?s6Ce&S@9&L~=2{6*AJ>1hTF*tk7}mF06P4DxUu8 z$BJiI#r-SK<>*%II;Wjkhc^MLe&vo;M-33WfVF#8z>t1yGsduy?;BgZAWBwx`JVd` z_pLG^o?bQgCdq1OU)jEQ+`tC((^d9+B&#-Bdm)cJrCu>)+6*&nFhjd?Q8LW7wa>wc z+UQTjOYlwq9qj#o>Q~xjbu=M9u3x^W0FCZ+5Yh{F0;~u8JwIoYZxCGJ z4_&r)3SGK3&X>71e~~Y_wL)}_Nu+?h*9a&lEAs)>#7+RUq!)u8QkDSNPnInK{F@xT z1JEeQrGUQ)CB=Zv@b@#I2mXErgc61^&vtXS8IY9BO23@K@a1Hty_8vl+QwBtO~RGNZe%$Yq*Qm#Vq<*#gGVIU&Rg+;A+K#Ie?-{ zrgh!H%FL^JD+Pkfs#x&JDmL@&DmL@hYKF5M@_gnU=Xkz`c}o^Be9Ymhx|6y8tYsVZ z*RthJb*$*^b!@@C8N^iiIwC_(y%&%rTM#*N6(U!*BJ$)15C!sPM3MY5Vy^5(l*xw> zm2v&`SsXbWxg2>2c>fBxTf{M!ql}}HV?IYMM?J?9j%6H8 z95#+-4!c9Z$9%tIrvuRD=tVr_IDvTB(F0l~S?}0`bDrSX$gzn-<#>i;8%GDn3jmt@ GuKa(wf;hnd diff --git a/runtime/include/fcntl.h b/runtime/include/fcntl.h new file mode 100644 index 0000000..8b665a1 --- /dev/null +++ b/runtime/include/fcntl.h @@ -0,0 +1,49 @@ +// fcntl.h -- file-control definitions for the GNO/ME runtime. +// +// O_* values match GNO's exactly so binaries agree with the +// kernel and shell. open()/creat() live in runtime/src/libcGno.c as GS/OS- +// toolbox emulation over the gsos* wrappers. +#ifndef _FCNTL_H +#define _FCNTL_H + +#include // mode_t / off_t (single source of truth) + +#ifdef __cplusplus +extern "C" { +#endif + +// Access modes (open-only), matching GNO . +#define O_RDONLY 0x0001 +#define O_WRONLY 0x0002 +#define O_RDWR 0x0004 +#define O_ACCMODE 0x0007 + +// Status / creation flags, matching GNO . +#define O_NONBLOCK 0x0008 +#define O_APPEND 0x0010 +#define O_CREAT 0x0020 +#define O_TRUNC 0x0040 +#define O_EXCL 0x0080 +#define O_BINARY 0x0100 // non-POSIX GNO flag: create as BIN not TXT +#define O_TRANS 0x0200 // GNO-specific (text translation) +#define O_NOCTTY 0 // no controlling-tty assignment (no bit) + +// whence values for lseek(); guarded so 's copies do not clash. +#ifndef SEEK_SET +#define SEEK_SET 0 +#endif +#ifndef SEEK_CUR +#define SEEK_CUR 1 +#endif +#ifndef SEEK_END +#define SEEK_END 2 +#endif + +int open(const char *path, int flags, ...); +int creat(const char *path, mode_t mode); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/runtime/include/signal.h b/runtime/include/signal.h index 85795f2..326f3f1 100644 --- a/runtime/include/signal.h +++ b/runtime/include/signal.h @@ -24,10 +24,56 @@ typedef void (*__sighandler_t)(int); #define SIGFPE 8 #define SIGSEGV 11 #define SIGTERM 15 +#define SIGALRM 14 // alarm-clock signal (GNO value; used by sleep/usleep) __sighandler_t signal(int sig, __sighandler_t handler); int raise(int sig); +// ---- POSIX signal surface (GNO/ME kernel-backed; see libcGno.c) -------- +#define NSIG 32 // GNO: signal numbers are 1..31 (mask is 1-32) + +#define SIGHUP 1 +#define SIGQUIT 3 +#define SIGTRAP 5 +#define SIGKILL 9 +#define SIGBUS 10 +#define SIGPIPE 13 +#define SIGSTOP 17 +#define SIGTSTP 18 +#define SIGCONT 19 +#define SIGCHLD 20 +#define SIGTTIN 21 +#define SIGTTOU 22 +#define SIGUSR1 30 +#define SIGUSR2 31 + +typedef unsigned long sigset_t; +typedef void (*sig_t)(int); + +#define SIG_BLOCK 0 +#define SIG_UNBLOCK 1 +#define SIG_SETMASK 2 + +struct sigaction { + void (*sa_handler)(int); + sigset_t sa_mask; + int sa_flags; +}; + +int kill(int pid, int sig); +int sigaction(int sig, const struct sigaction *act, struct sigaction *oact); +int sigprocmask(int how, const sigset_t *set, sigset_t *oset); +int sigsuspend(const sigset_t *mask); +unsigned long sigblock(unsigned long mask); +unsigned long sigsetmask(unsigned long mask); + +// Pure-C bitset ops (GNO defines these as macros, not functions). +#define sigemptyset(set) (*(set) = 0, 0) +#define sigfillset(set) (*(set) = ~(sigset_t)0, 0) +#define sigaddset(set, signo) (*(set) |= (sigset_t)1 << ((signo) - 1), 0) +#define sigdelset(set, signo) (*(set) &= ~((sigset_t)1 << ((signo) - 1)), 0) +#define sigismember(set, signo) ((*(set) & ((sigset_t)1 << ((signo) - 1))) != 0) + #ifdef __cplusplus } #endif diff --git a/runtime/include/stdlib.h b/runtime/include/stdlib.h index 57194c5..4571fe9 100644 --- a/runtime/include/stdlib.h +++ b/runtime/include/stdlib.h @@ -61,10 +61,15 @@ typedef void (*__atexit_fn)(void); int atexit(__atexit_fn fn); int at_quick_exit(__atexit_fn fn); -// No environment under GS/OS — `getenv` always returns NULL, -// `system` always returns 0 (no shell to invoke). These exist for -// portable-code compile compatibility. +// Environment: bare GS/OS has none, so libc.c's weak `getenv` returns NULL and +// `system` returns 0. Under GNO (libcGno.o linked) getenv/setenv/putenv/ +// unsetenv are backed by the kernel shell-variable table. `environ` stays NULL +// in that thin port -- use the accessors, not environ[] directly. char *getenv(const char *name); +int setenv(const char *name, const char *value, int overwrite); +int unsetenv(const char *name); +int putenv(const char *str); +extern char **environ; int system(const char *cmd); #define EXIT_SUCCESS 0 diff --git a/runtime/include/sys/stat.h b/runtime/include/sys/stat.h new file mode 100644 index 0000000..14545b7 --- /dev/null +++ b/runtime/include/sys/stat.h @@ -0,0 +1,100 @@ +// sys/stat.h -- file status for the GNO/ME runtime. +// +// struct stat MUST byte-match the layout the GNO kernel's stat.c writes +// (kern/gno/stat.c, compiled by ORCA/C with 16-bit int / 32-bit long). +// Kstat/Kfstat/Klstat (gno/kernel.h) fill this buffer directly, so any +// field-order or size drift silently corrupts every returned field. The +// _Static_assert below pins sizeof to the ORCA/C layout (54 bytes); the +// field offsets all land on even boundaries, so our 2-byte i32 alignment +// (datalayout i32:16) inserts no padding and matches ORCA/C's packing. +// (Verified: sizeof==54, offsetof(st_size)==16, st_mtime==26, st_blocks==42, +// matching the __GNO__ branch of tools/gno/src-repo/include/sys/stat.h.) +// +// Field semantics follow GNO's kernel: st_nlink/st_uid/st_gid/st_rdev are +// always 0, st_ino is a per-call fake, times are Unix epoch seconds, and +// st_mode carries the S_IF* type bits plus ProDOS-derived permission bits. +#ifndef _SYS_STAT_H_ +#define _SYS_STAT_H_ + +#include +#include // time_t (single source of truth) + +#ifdef __cplusplus +extern "C" { +#endif + +struct stat { + dev_t st_dev; // device number + ino_t st_ino; // (faked) inode number + unsigned short st_mode; // file type + permission bits + short st_nlink; // link count (always 0 under GNO) + uid_t st_uid; // owner uid (always 0) + gid_t st_gid; // owner gid (always 0) + dev_t st_rdev; // device type (always 0) + off_t st_size; // file size in bytes + time_t st_atime; // last access time (== mod time) + int st_spare1; + time_t st_mtime; // last modification time + int st_spare2; + time_t st_ctime; // creation / status-change time + int st_spare3; + long st_blksize; // preferred I/O block size (512) + long st_blocks; // 512-byte blocks allocated + long st_spare4[2]; +}; + +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +_Static_assert(sizeof(struct stat) == 54, "struct stat must byte-match the GNO kernel layout"); +#endif + +// File type mask + type values (ProDOS/GS-OS mapped by the kernel). +#define S_IFMT 0170000 // type-of-file mask +#define S_IFIFO 0010000 // named pipe (fifo) +#define S_IFCHR 0020000 // character special (TTY / device) +#define S_IFDIR 0040000 // directory +#define S_IFBLK 0060000 // block special +#define S_IFREG 0100000 // regular file +#define S_IFLNK 0120000 // symbolic link +#define S_IFSOCK 0140000 // socket (GNO reports pipes as this) + +// Permission bits. +#define S_ISUID 0004000 // set-user-id on execution +#define S_ISGID 0002000 // set-group-id on execution +#define S_ISVTX 0001000 // sticky bit + +#define S_IRWXU 0000700 // RWX mask for owner +#define S_IRUSR 0000400 // R for owner +#define S_IWUSR 0000200 // W for owner +#define S_IXUSR 0000100 // X for owner +#define S_IREAD S_IRUSR +#define S_IWRITE S_IWUSR +#define S_IEXEC S_IXUSR + +#define S_IRWXG 0000070 // RWX mask for group +#define S_IRGRP 0000040 // R for group +#define S_IWGRP 0000020 // W for group +#define S_IXGRP 0000010 // X for group + +#define S_IRWXO 0000007 // RWX mask for other +#define S_IROTH 0000004 // R for other +#define S_IWOTH 0000002 // W for other +#define S_IXOTH 0000001 // X for other + +// File-type test macros. +#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) +#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) +#define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK) +#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) +#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) +#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) +#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK) + +int stat(const char *path, struct stat *sb); +int fstat(int fd, struct stat *sb); +int lstat(const char *path, struct stat *sb); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/runtime/include/sys/times.h b/runtime/include/sys/times.h new file mode 100644 index 0000000..9af3a31 --- /dev/null +++ b/runtime/include/sys/times.h @@ -0,0 +1,25 @@ +// sys/times.h -- process CPU-time accounting for the GNO/ME runtime. +// +// Layout byte-matches GNO's own (tools/gno kernel fills it): +// four clock_t fields, clock_t == unsigned long (32-bit) on this target, +// so sizeof(struct tms) == 16 on both sides. times() maps to the GNO +// kernel Ktimes primitive (toolset $03, func $35), which fills utime/cutime +// with the process' 60 Hz tick counts and leaves stime/cstime zero. +#ifndef _SYS_TIMES_H +#define _SYS_TIMES_H + +// Single source of truth for clock_t (avoids a duplicate typedef). +#include + +struct tms { + clock_t tms_utime; // user CPU time (60 Hz ticks) for this process + clock_t tms_stime; // system CPU time (always 0 under GNO) + clock_t tms_cutime; // user CPU time of reaped child processes + clock_t tms_cstime; // system CPU time of reaped children (always 0) +}; + +_Static_assert(sizeof(struct tms) == 16, "struct tms must match GNO kernel layout"); + +clock_t times(struct tms *buf); + +#endif diff --git a/runtime/include/sys/types.h b/runtime/include/sys/types.h new file mode 100644 index 0000000..1f0f8d0 --- /dev/null +++ b/runtime/include/sys/types.h @@ -0,0 +1,20 @@ +// sys/types.h -- primitive POSIX id/size typedefs for the GNO/ME runtime. +// +// Single source of truth for the scalar types struct stat and the file +// syscalls are built from. Sizes are pinned to the GNO ABI (int == 16 +// bits, long == 32 bits), so a struct laid out from these types byte- +// matches what the GNO kernel writes. time_t lives in (its own +// single source); it is not redefined here. +#ifndef _SYS_TYPES_H_ +#define _SYS_TYPES_H_ + +typedef unsigned short dev_t; // device number (2 bytes) +typedef unsigned long ino_t; // inode number (4 bytes) +typedef unsigned short uid_t; // user id (2 bytes) +typedef unsigned short gid_t; // group id (2 bytes) +typedef unsigned short mode_t; // permission bits (2 bytes) +typedef unsigned short nlink_t; // hard-link count (2 bytes) +typedef long off_t; // file offset / size (4 bytes) +typedef int pid_t; // process id (2 bytes) + +#endif diff --git a/runtime/include/sys/wait.h b/runtime/include/sys/wait.h new file mode 100644 index 0000000..8f691d6 --- /dev/null +++ b/runtime/include/sys/wait.h @@ -0,0 +1,28 @@ +// sys/wait.h -- GNO/ME process-wait status decoding. +// +// GNO's wait status is a single 16-bit int (sizeof(int)==2 on this target), the +// BSD "union wait" word (see tools/gno/src-repo/include/sys/wait.h): +// bits 0-6 termination signal (0 => normal exit) +// bit 7 core-dump flag (always 0 under GNO) +// bits 8-15 exit code (valid when the termination signal is 0) +// So a normal exit(N) yields status (N << 8); WEXITSTATUS shifts it back down. +#ifndef _SYS_WAIT_H +#define _SYS_WAIT_H + +#define WIFEXITED(s) (((s) & 0x7F) == 0) +#define WEXITSTATUS(s) (((s) >> 8) & 0xFF) +#define WIFSIGNALED(s) (((s) & 0x7F) != 0 && ((s) & 0x7F) != 0x7F) +#define WTERMSIG(s) ((s) & 0x7F) +#define WIFSTOPPED(s) (((s) & 0x7F) == 0x7F) +#define WSTOPSIG(s) (((s) >> 8) & 0xFF) +#define WCOREDUMP(s) 0 + +// Option bits for waitpid's third argument. WNOHANG cannot be honored (the GNO +// kernel wait() blocks); waitpid rejects it. +#define WNOHANG 1 +#define WUNTRACED 2 + +int wait(int *status); +int waitpid(int pid, int *status, int options); + +#endif diff --git a/runtime/include/unistd.h b/runtime/include/unistd.h new file mode 100644 index 0000000..7609920 --- /dev/null +++ b/runtime/include/unistd.h @@ -0,0 +1,87 @@ +// unistd.h -- POSIX process, credential, job-control + timing glue for the +// GNO/ME runtime. These wrap the GNO kernel toolset $03 primitives (see +// gno/kernel.h) into POSIX names; the bodies live in runtime/src/libcGno.c. +// File/stat decls live in / . +#ifndef _UNISTD_H +#define _UNISTD_H + +#include +#include // single source of truth for pid_t/uid_t/gid_t/off_t + +#ifdef __cplusplus +extern "C" { +#endif + +// usleep granularity argument. 32-bit so callers can request more than the +// 65 ms an int would cap at, even though the kernel floors us at 0.1 s. +typedef unsigned long useconds_t; + +// Standard stream fd numbers (POSIX Unix values; gnoFd remaps to GNO 1/2/3). +#define STDIN_FILENO 0 +#define STDOUT_FILENO 1 +#define STDERR_FILENO 2 + +// ---- process control (runtime/src/libcGno.c) ---- +int fork(void *subr); // GNO thread-spawn (subr = (void *)&leafFn) +int getpid(void); +int getppid(void); +int dup(int oldfd); +int dup2(int oldfd, int newfd); +int pipe(int fds[2]); +int isatty(int fd); // terminal test via fstat + S_ISCHR + +// ---- credentials (runtime/src/libcGno.c) ---- +uid_t getuid(void); +uid_t geteuid(void); +gid_t getgid(void); +gid_t getegid(void); +int setuid(uid_t uid); +int setgid(gid_t gid); + +// ---- job control (runtime/src/libcGno.c) ---- +pid_t getpgrp(void); +int setpgid(pid_t pid, pid_t pgrp); +int setpgrp(pid_t pid, pid_t pgrp); // BSD alias of setpgid (same 0x3403 entry) +int tcsetpgrp(int fd, pid_t pgrp); +pid_t tcgetpgrp(int fd); // stub: ENOSYS (kernel routes TIOCGPGRP to `invalid`) +pid_t setsid(void); // stub: ENOSYS (no GNO session primitive) + +// ---- raw I/O + file (bodies in libcGno.c) ---- +long read(int fd, void *buf, unsigned long n); +long write(int fd, const void *buf, unsigned long n); +int close(int fd); +off_t lseek(int fd, off_t offset, int whence); +int unlink(const char *path); + +// ---- file access test (runtime/src/libcGno.c) ---- +// Only F_OK (existence) is honored under GNO; R/W/X_OK degrade to an existence +// test (GNO stat reports no meaningful owner/perm bits). +#define F_OK 0 // test for existence of the file +#define X_OK 0x01 // test for execute/search permission +#define W_OK 0x02 // test for write permission +#define R_OK 0x04 // test for read permission +int access(const char *path, int mode); + +// ---- exec family (runtime/src/libcGno.c) ---- +// GNO's kernel exec takes ONE flattened command line (not argv/envp): these +// space-join argv, stripping argv[0]'s GS/OS prefix, and IGNORE envp. On +// success they replace the process image and do not return; on failure they +// return -1 with errno set. execvp/execlp resolve a bare name through $PATH. +int execv (const char *path, char *const *argv); +int execve(const char *path, char *const *argv, char *const *envp); +int execvp(const char *file, char *const *argv); +int execl (const char *path, const char *arg, ...); +int execle(const char *path, const char *arg, ...); +int execlp(const char *file, const char *arg, ...); + +// ---- timing ---- +unsigned long alarm(unsigned long seconds); +unsigned int sleep(unsigned int seconds); +int usleep(useconds_t useconds); +int pause(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/runtime/src/crt0Gno.s b/runtime/src/crt0Gno.s index c8d46ec..8381c9e 100644 --- a/runtime/src/crt0Gno.s +++ b/runtime/src/crt0Gno.s @@ -3,12 +3,17 @@ ; Use this INSTEAD OF crt0.s / crt0Gsos.s when building an OMF intended ; to run as a GNO shell command (filetype $B5 EXE, aux 0). ; -; GNO/ME entry contract for a shell command (per ORCA's ~GNO_COMMAND): +; GNO/ME entry contract for a shell command (per ORCA's ~GNO_COMMAND, +; verified against tools/gno/src-repo/lib/libc/gno/gnocmd.asm: it does +; `sta ~USER_ID` / `sty ~COMMANDLINE` / `stx ~COMMANDLINE+2`): ; E=0 (native), M=0 (16-bit accumulator), X=0 (16-bit index) -; A:X = command line pointer (32-bit; A=low word, X=high word) -; Y = user ID (currently unused by us) +; A = user ID +; Y:X = command line pointer (32-bit; Y=low word, X=high word) ; DBR = bank of our entry segment (set by GS/OS Loader) -; DP = pointer to a Memory-Manager-allocated DP page +; DP = a Memory-Manager-allocated, PER-PROCESS direct page. We HONOR +; it (do NOT force DP=0) so codegen's DP slots are process-private +; under GNO's preemptive multitasking — see the __start body. +; (~GNO_COMMAND itself keeps this DP and stores argv/argc in it.) ; The GS/OS Loader's launch frame on the stack is discarded by Quit. ; ; Differences from crt0Gsos.s: @@ -23,42 +28,51 @@ .globl __start __start: - ; GNO entry contract (from kern ~GNO_COMMAND, lib/libc/gno/gnocmd.asm): - ; A = user ID - ; Y:X = command-line pointer (Y = low word, X = high word) — a - ; NUL-terminated C string of the invocation command line. - ; Stash the cmdline pointer (Y:X) to bank-0 scratch $00:00B0..B3 - ; and the user ID (A) to $00:00B6, BEFORE we switch to DP=0. - ; Bank-explicit `sta long` (force-long `>` prefix) so we don't depend - ; on DP/DBR here. + ; MULTITASKING: HONOR GNO's per-process direct page -- do NOT force + ; DP=0. GNO is preemptive. Every codegen DP slot (the __jsl_indir + ; vector at $B8, the PBR stash at $BE, the libcall scratch at + ; $E0..$FF) and our own cmdline/user-ID stash at $B0..$B7 below are + ; DP-relative, so keeping DP at GNO's per-process page makes them all + ; PROCESS-PRIVATE. Forcing DP=0 (as this crt used to) put every one + ; of them on the shared bank-0 zero page -- so two of our binaries run + ; concurrently (e.g. a `a | b` pipeline that gsh forks) aliased each + ; other's saved DP / pointer bank / scratch -> silent cross-process + ; corruption. This mirrors crt0Gsos.s honoring the Loader DP; the + ; backend has no DP=0 requirement (it addresses every scratch slot + ; DP-relative). (GNO's GS/OS interceptor identifies the caller by + ; curProcInd -- scheduler state, not DP -- and StackGSOS saves/restores + ; the caller's DP itself, so honoring GNO's DP is purely about keeping our + ; own scratch process-private, not about being identified -- see + ; gnoGsos.s.) + ; + ; Stash cmdline (Y:X) at DP $B0..$B3 and user ID (A) at $B6, DP-form so + ; they land in our private page. DP addressing uses D (valid on entry) + ; and ignores DBR, so this is safe before the phk/plb below. rep #0x30 - sta >$0000B6 ; sta long $00:00B6 (user ID) + sta 0xb6 ; DP $B6 = user ID (per-process) tya - sta >$0000B0 ; sta long $00:00B0 (cmdline low word = Y) + sta 0xb0 ; DP $B0 = cmdline low word (= Y) txa - sta >$0000B2 ; sta long $00:00B2 (cmdline high word = X) + sta 0xb2 ; DP $B2 = cmdline high word (= X) ; Set DBR := PBR (our entry-segment bank) — same Loader-contract ; mitigation as crt0Gsos.s. phk plb - ; Save GNO's per-process direct page BEFORE zeroing it. GNO's - ; GS/OS-call interceptor uses the caller's DP to identify the - ; calling process (and find its fd table). Our codegen needs - ; DP=0, so we stash GNO's DP at $00:00B4 and restore it around - ; GS/OS calls (see __gnoSaveDP / gsosWrite-under-GNO path). + ; Point the libgcc __jsl_indir trampoline's JMP-(abs) operand at + ; (DP + $B8) so indirect dispatch reads its target from our private DP + ; page instead of bank-0 $00B8. Identical to crt0Gsos.s and REQUIRED + ; now that DP is non-zero (the trampoline operand defaults to $00B8). rep #0x30 - tdc ; A = current (GNO) DP - sta >$0000B4 ; sta long $00:00B4 (stash GNO DP word) - - ; Set DP=0 — backend assumes DP=0 for `sta dp` / `[dp],y` etc. - lda #0 - tcd + tdc + clc + adc #0xb8 + sta __jsl_indir_op ; Persistent "current data bank" byte at DP $BE = PBR (our bank). - ; See crt0.s comment for rationale (codegen reads this for the - ; bank half of `&symbol` 32-bit pointers). + ; See crt0.s comment (codegen reads DP $BE for the bank half of + ; `&symbol` 32-bit pointers). DP-form -> per-process. sep #0x20 phk pla @@ -66,20 +80,42 @@ __start: stz 0xbf ; pad so `lda 0xbe` in 16-bit M reads $00PBR rep #0x20 - ; BSS zero-init (DBR-relative byte stores). M is held at 8 across - ; the whole loop while X stays 16-bit, so we don't flip SEP/REP per - ; byte (llvm-mc still encodes cpx/ldx as 16-bit X-immediates in M=8). - rep #0x30 ; M=16, X=16 - sep #0x20 ; M=8 for the byte stores; X remains 16-bit - ldx #__bss_start -.Lbss_loop: - cpx #__bss_end - bcs .Lbss_done - stz 0x0000, x ; 1-byte store (M=8) - inx - bra .Lbss_loop -.Lbss_done: - rep #0x20 ; restore M=16 + ; BSS zero-init: NOT NEEDED under GNO -- and actively harmful. omfEmit + ; embeds the BSS region as LCONST zeros inside the code segment (seg1 + ; LENGTH covers [text .. __bss_end], RESSPC=0), so the GS/OS InitialLoad + ; that GNO's exec uses fills BSS with zeros at load time -- BSS is + ; already zero when __start runs (verified: cxxChronoProbe's zero-init + ; clock statics read correctly with this loop gone). + ; + ; The old DBR-relative `stz $0000,X` loop over [__bss_start, __bss_end] + ; was redundant AND corrupted GNO's heartbeat task queue: once BSS grew + ; large (cxxChronoProbe, ~6 KB BSS, __bss_end ~0xB74C) the clear reached + ; the kernel's bank-0 heartbeat block (alarmHB), and the next VBL tick + ; hit SysFailMgr $0308 "Damaged heartbeat queue" -> system halt BEFORE + ; main. A non-chrono probe with an equally large BSS reproduced it, so + ; it is a pure BSS-ceiling collision, not a chrono bug. Small programs + ; (BSS below ~0xB100) escaped, which is why it went unnoticed. This is + ; the same fix crt0Gsos.s already applied for the analogous GS/OS + ; Memory-Manager corruption -- see feedback_gsos_fopen_partial_diagnosis. + + ; Back the C malloc heap with an MM-owned handle -- the SAME fix + ; crt0Gsos.s applies. crt0Gno historically kept malloc on link816's + ; fixed link-time window [__bss_end, 0xBF00) in BANK 0, but that span is + ; PHANTOM memory the Memory Manager never reserved for us: it runs + ; straight through GNO's high-bank-0 kernel region (the heartbeat block + ; alarmHB ~0xB7xx), so any GNO program that mallocs a few KB reproduces + ; the same SysFailMgr $0308 halt the BSS clear did. __heapInitMM + ; NewHandles a locked block in MM-owned (safe-bank) memory and repoints + ; malloc at it. Our GNO process MM user ID was handed to __start in A and + ; stashed at $00:00B6; GNO already started the Memory Manager (see NOTE + ; below), so -- unlike crt0Gsos -- we must NOT call MMStartUp, we reuse + ; the handed-in ID (read DP-relative from our private page). NewHandle + ; keys ownership off the userID PARAMETER, not DP. On NewHandle failure + ; __heapInitMM leaves malloc on the link-time window (no worse than + ; before). crt0.s (bare-metal, no MM) still does not do this. + rep #0x30 + lda 0xb6 ; DP $B6 = our GNO process MM user ID (per-process) + jsl __heapInitMM ; __heapInitMM(userID); arg0 in A ; Walk .init_array (C++ ctors). Inlined from crt0Gsos.s. The ; `jsl __jsl_indir` and `jsl main` operands are relocated by the @@ -119,12 +155,12 @@ __start: rep #0x30 jsl __srandInitFromTime - ; Reload cmdline ptr from $00:00B0..$00:00B3 into A:X. - ; Use bank-explicit `lda long` so we don't depend on DBR. + ; Reload cmdline ptr from our private DP page ($B0..$B3) into A:X. + ; DP-form (not `lda long`) so we read the per-process copy. rep #0x30 - .byte 0xaf, 0xb0, 0x00, 0x00 ; lda long $00:00B0 -> A (cmdline lo word) + lda 0xb0 ; DP $B0 -> A (cmdline lo word) pha ; stash A on stack briefly - .byte 0xaf, 0xb2, 0x00, 0x00 ; lda long $00:00B2 -> A (cmdline hi word) + lda 0xb2 ; DP $B2 -> A (cmdline hi word) tax ; X = cmdline hi pla ; A = cmdline lo diff --git a/runtime/src/crt0Gsos.s b/runtime/src/crt0Gsos.s index 3cdb4a8..ee01dbe 100644 --- a/runtime/src/crt0Gsos.s +++ b/runtime/src/crt0Gsos.s @@ -98,9 +98,11 @@ __start: ; system's) can place a block on top of live malloc data (JoeyLib ; item 5, heap channel). MMStartUp ($0202) returns our Memory-Manager ; user ID in A; pass it as arg0 to __heapInitMM (strong impl in libc.o; - ; weak no-op in libgcc.s for programs that don't link libc). crt0.s / - ; crt0Gno.s do NOT do this -- bare-metal has no Memory Manager and GNO - ; manages its own heap; both keep malloc's link-time window. + ; weak no-op in libgcc.s for programs that don't link libc). crt0Gno.s + ; does the SAME (it reuses the MM user ID GNO hands it in A -- stashed at + ; DP $B6 -- instead of calling MMStartUp, since GNO already started the + ; Memory Manager). Only crt0.s (bare-metal, no Memory Manager) skips this + ; and keeps malloc's link-time window. rep #0x30 pha ; MMStartUp result space (word) ldx #0x0202 ; MMStartUp call number diff --git a/runtime/src/gnoGsos.s b/runtime/src/gnoGsos.s index 2061eb6..70a8122 100644 --- a/runtime/src/gnoGsos.s +++ b/runtime/src/gnoGsos.s @@ -27,16 +27,134 @@ __gnoGsosCall: sta __gnoPBlock+2 ; inline pBlock bank+pad (X = bank : pad) lda 4, s ; callNum from the stack sta __gnoCallNum ; inline callNum - lda 0xb4 ; restore GNO's per-process DP - tcd + ; DP is ALREADY our per-process page: crt0Gno honors GNO's DP and never + ; forces DP=0, so codegen's bank-0 DP scratch stays process-private and + ; two of our binaries running concurrently no longer alias a shared $B4 + ; saved DP. GNO's interceptor keys the caller off curProcInd (scheduler + ; state), NOT off DP: StackGSOS (kern/gno/gsos.asm) does `ldx curProcInd`, + ; brackets the call with its own phd/pld, and installs its own DP work + ; area, so the caller's DP is preserved by GNO regardless of its value. + ; The phd/pld here is a harmless-redundant belt-and-suspenders; the GS/OS + ; error is returned in A (0 = OK) and pld does not touch A. + phd jsl 0xe100a8 __gnoCallNum: .word 0 ; patched: GS/OS call number __gnoPBlock: .long 0 ; patched: param-block pointer - ; --- OurGSOS returns here (return addr bumped +6) --- - tay ; Y = error (survives DP change) - lda #0 - tcd ; DP := 0 for the C caller - tya + ; --- OurGSOS returns here (return addr bumped +6); error in A --- + pld ; restore our per-process DP (A = error, untouched) rtl + + +; __gnoQuit(void *pBlock, int status) -> noreturn +; GS/OS QUIT ($2029) that carries the REAL exit status. Same inline-dispatch +; trick as __gnoGsosCall, but with ONE critical difference: the call number is +; a hardcoded inline CONSTANT (.word 0x2029), leaving A free to hold `status` +; at the trap. GNO's StackGSOS does `ldx curProcInd; sta exitCode,x` on entry +; (kern/gno/gsos.asm), so whatever is in A at the JSL is recorded as this +; process's exit code and later decoded by the parent's wait() (WEXITSTATUS). +; __gnoGsosCall loads A = callNum right before its trap, so routing QUIT +; through it recorded EVERY process's exit status as 0x2029 -> wait() saw +; w_retcode = 0x2029 & 0xFF = 0x29 (41). This helper fixes that. +; C ABI: arg0 (pBlock ptr32) in A:X (A=offset, X=bank); arg1 (status i16) at +; (4,s). QUIT reaps the process and returns to the launcher -- never here. + .globl __gnoQuit +__gnoQuit: + rep #0x30 + sta __gnoQuitPB ; inline pBlock offset (low 16) = A + txa + sta __gnoQuitPB+2 ; inline pBlock bank+pad (X = bank : pad) + lda 4, s ; A = status (arg1) -- StackGSOS stashes it as exitCode + jsl 0xe100a8 + .word 0x2029 ; inline callNum = GS/OS QUIT (CONSTANT, never via A) +__gnoQuitPB: + .long 0 ; patched: param-block pointer + ; QUIT never returns; if it somehow does, spin rather than run off the end. +.LgnoQuitSpin: + bra .LgnoQuitSpin + + +; __sigTrampoline -- the GNO kernel's signal-handler callback ABI shim. +; +; signal() installs THIS as the kernel handler (via Ksignal) for every caught +; signal, with the real C handler stashed in userHandlers[sig]. The kernel +; delivers a signal by either (case 1, raise/self-signal) calling the handler +; directly from KERNkill, or (case 2, async, e.g. an expired alarm) building a +; frame on the process stack that RTIs to the handler. BOTH paths present the +; SAME ORCA/C frame at handler entry (kern/gno/signal.c KERNkill + ctool.asm +; ctxtRestore): +; entry S=S0, native, m=x=0: +; [S0+1..S0+3] = 24-bit RTL return (case2: ctxtRestore-1; case1: KERNkill) +; [S0+4..S0+5] = param word #1 = the SIGNAL NUMBER +; [S0+6..S0+7] = param word #2 (0) +; ORCA/C handlers are CALLEE-POPS: on return the handler removes its 2 param +; words (4 bytes) and RTLs, leaving S=S0+7. Our handlers are caller-pops with +; arg0 in A, so this trampoline calls the C handler with the signal number in A, +; then does the ORCA callee-pop return the kernel expects. It also swaps to our +; process DP (case 1 enters in the KERNEL's DP) and restores it on the way out. + .globl __sigTrampoline +__sigTrampoline: + rep #0x30 ; force 16-bit A/X/Y (kernel entry is native m=x=0) + lda 4, s ; A = signal number (param word #1) -- before any push + tay ; Y = signal number (survives the pushes below) + phb ; save incoming DBR + phd ; save incoming DP + lda >__gnoProcDP ; A = this process's direct page (abs-long) + tcd ; D = our process DP + phk ; \ DBR = our program bank (PBR of this trampoline) + plb ; / + tya ; A = signal number + asl a ; \ X = sig * 4 (userHandlers[] entries are 4-byte) + asl a ; / + tax + lda >userHandlers, x ; low 16 of handler ptr (abs-long indexed) + ora >userHandlers+2, x ; | bank word + beq .LsigSkip ; no handler (raced to SIG_DFL/IGN) -> just return + ; --- push a return frame so the C handler RTLs back to .LsigTrampBack --- + phk ; handler-return bank = our PBR + per .LsigTrampBack-1 ; handler-return offset (PC-relative -> reloc-safe) + ; --- push the handler address-1 as a 24-bit RTL-jump target --- + sep #0x20 + lda >userHandlers+2, x ; handler bank byte + pha + rep #0x20 + lda >userHandlers, x ; handler offset + dec a ; -1 (RTL adds 1) + pha + tya ; A = signal number (handler arg0, our ABI) + rtl ; -> C handler; it RTLs back to .LsigTrampBack +.LsigTrampBack: +.LsigSkip: + pld ; restore incoming DP + plb ; restore incoming DBR (S now back to entry S0) + ; --- ORCA callee-pop return: move the 3-byte RTL return up over the 4 --- + ; --- param bytes, discard them, RTL to the kernel, leaving S=S0+7. --- + rep #0x30 + lda 1, s ; ret low 16 ([S0+1,S0+2]) + sta 5, s ; -> [S0+5,S0+6] + sep #0x20 + lda 3, s ; ret bank ([S0+3]) + sta 7, s ; -> [S0+7] + rep #0x20 + tsc + clc + adc #4 ; drop the 4 param bytes + tcs + rtl ; -> ctxtRestore (case2) / KERNkill (case1) + + +; __gnoCaptureDP -- record this process's direct page for __sigTrampoline. +; Called from signal() (normal process context, D = our per-process page). + .globl __gnoCaptureDP +__gnoCaptureDP: + rep #0x30 + tdc ; A = D (our process direct page) + sta >__gnoProcDP ; abs-long store (bank-safe) + rtl + + + .data + .globl __gnoProcDP +__gnoProcDP: + .word 0 ; our process DP; set by __gnoCaptureDP before delivery diff --git a/runtime/src/gnoKernel.s b/runtime/src/gnoKernel.s index 98cfec3..45d92aa 100644 --- a/runtime/src/gnoKernel.s +++ b/runtime/src/gnoKernel.s @@ -1,7 +1,7 @@ ; AUTOGENERATED by scripts/genGnoKernel.py — DO NOT EDIT by hand. ; ; GNO/ME kernel toolset $03 wrappers. -; Dispatcher: JSL $E10000 with LDX #funcId. +; Dispatcher: JSL $E10008 (GNO's udispatch vector) with LDX #funcId. ; ; C ABI: arg0 (i16) in A, arg0 (i32) in A:X, arg1+ on stack ; (caller-pushed, lo-word first for 32-bit values). @@ -18,7 +18,7 @@ Kgetpid: ; --- result space (2 bytes) --- pea 0 ldx #0x0903 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -43,7 +43,7 @@ Kkill: lda 14, s pha ldx #0x0A03 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -68,7 +68,7 @@ Kfork: lda 12, s pha ldx #0x0B03 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -80,7 +80,7 @@ Kgetpid_dup: ; --- result space (2 bytes) --- pea 0 ldx #0x0903 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -100,7 +100,7 @@ Kgetppid: lda 0xE0 pha ldx #0x4003 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -125,7 +125,7 @@ Kwait: lda 12, s pha ldx #0x1703 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -155,7 +155,7 @@ K_execve: lda 20, s pha ldx #0x1D03 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -183,7 +183,7 @@ Ksignal: lda 20, s pha ldx #0x1603 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A plx ; result hi -> X rtl @@ -210,7 +210,7 @@ Kalarm: lda 14, s pha ldx #0x1E03 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A plx ; result hi -> X rtl @@ -237,7 +237,7 @@ Kalarm10: lda 14, s pha ldx #0x4203 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A plx ; result hi -> X rtl @@ -263,7 +263,7 @@ Ksigpause: lda 12, s pha ldx #0x2103 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -289,7 +289,7 @@ Ksigsetmask: lda 14, s pha ldx #0x1B03 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A plx ; result hi -> X rtl @@ -316,7 +316,7 @@ Ksigblock: lda 14, s pha ldx #0x1C03 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A plx ; result hi -> X rtl @@ -339,7 +339,7 @@ Kdup: lda 10, s pha ldx #0x2203 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -364,7 +364,7 @@ Kdup2: lda 14, s pha ldx #0x2303 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -389,7 +389,7 @@ Kpipe: lda 12, s pha ldx #0x2403 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -421,7 +421,7 @@ Kioctl: lda 26, s pha ldx #0x2603 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -451,7 +451,7 @@ Kstat: lda 20, s pha ldx #0x2703 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -478,7 +478,7 @@ Kfstat: lda 18, s pha ldx #0x2803 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -508,7 +508,7 @@ Klstat: lda 20, s pha ldx #0x2903 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -528,7 +528,7 @@ Kgetuid: lda 0xE0 pha ldx #0x2A03 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -548,7 +548,7 @@ Kgetgid: lda 0xE0 pha ldx #0x2B03 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -568,7 +568,7 @@ Kgeteuid: lda 0xE0 pha ldx #0x2C03 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -588,7 +588,7 @@ Kgetegid: lda 0xE0 pha ldx #0x2D03 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -610,7 +610,7 @@ Ksetuid: lda 10, s pha ldx #0x2E03 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -632,7 +632,7 @@ Ksetgid: lda 10, s pha ldx #0x2F03 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -654,7 +654,7 @@ Ktcnewpgrp: lda 10, s pha ldx #0x1803 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -676,7 +676,7 @@ Ksettpgrp: lda 10, s pha ldx #0x1903 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -701,7 +701,7 @@ Ktctpgrp: lda 14, s pha ldx #0x1A03 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -723,7 +723,7 @@ K_getpgrp: lda 10, s pha ldx #0x2503 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -748,7 +748,7 @@ Ksetpgrp: lda 14, s pha ldx #0x3403 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -768,7 +768,7 @@ Kkvm_open: lda 0xE0 pha ldx #0x1103 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -793,7 +793,7 @@ Kkvm_close: lda 12, s pha ldx #0x1203 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A rtl @@ -819,7 +819,7 @@ Ktimes: lda 14, s pha ldx #0x3503 - jsl 0xe10000 + jsl 0xe10008 pla ; result lo -> A plx ; result hi -> X rtl @@ -848,5 +848,5 @@ KSetGNOQuitRec: lda 20, s pha ldx #0x4103 - jsl 0xe10000 + jsl 0xe10008 rtl diff --git a/runtime/src/libc.c b/runtime/src/libc.c index e43c672..1277b21 100644 --- a/runtime/src/libc.c +++ b/runtime/src/libc.c @@ -549,14 +549,16 @@ static void mallocInitOnce(void) { __heap_end ? __heap_end : HEAP_DEFAULT_END); } -// GS/OS: back the malloc heap with a locked Memory-Manager handle so the -// heap span is owned by the MM and no later NewHandle (ours or the -// system's) can land on top of live malloc data. crt0Gsos calls this with -// our MM user ID in A, before .init_array. crt0.s (bare-metal, no Memory -// Manager) and crt0Gno.s do NOT call it, so -ffunction-sections GC drops -// this function -- and its NewHandle reference -- from those links. A weak -// no-op __heapInitMM in libgcc.s resolves crt0Gsos's `jsl` for GS/OS -// programs that don't link libc.o. +// GS/OS and GNO: back the malloc heap with a locked Memory-Manager handle +// so the heap span is owned by the MM and no later NewHandle (ours or the +// system's) can land on top of live malloc data. crt0Gsos and crt0Gno both +// call this with our MM user ID in A, before .init_array -- crt0Gsos runs +// MMStartUp first to fetch the ID; crt0Gno reuses the ID GNO hands __start +// (GNO already started the Memory Manager). Only crt0.s (bare-metal, no +// Memory Manager) does NOT call it, so -ffunction-sections GC drops this +// function -- and its NewHandle reference -- from bare-metal links alone. A +// weak no-op __heapInitMM in libgcc.s resolves the `jsl` for GS/OS programs +// that don't link libc.o. // // This reservation is deliberately eager (done once at startup, before any // malloc) so the Memory Manager cannot hand the heap span to another @@ -570,10 +572,10 @@ static void mallocInitOnce(void) { extern void *NewHandle(unsigned long size, unsigned short userID, unsigned short attr, void *loc); extern void DisposeHandle(void *h); void __heapInitMM(unsigned short userID) { - // Idempotency guard: keep the first window that was installed. The sole - // caller (crt0Gsos) invokes this once before any malloc, but this keeps - // a second call -- or a future reorder -- from stomping a heap that is - // already serving allocations. + // Idempotency guard: keep the first window that was installed. Each + // entry crt (crt0Gsos for GS/OS, crt0Gno for GNO) invokes this once + // before any malloc, but this keeps a second call -- or a future reorder + // -- from stomping a heap that is already serving allocations. if (bumpPtr) { return; } @@ -1367,6 +1369,8 @@ void __assert_fail(const char *expr, const char *file, unsigned int line, } // ---- abort ---- +// Weak: a GNO link (libcGno.o) overrides with a raise(SIGABRT)-based abort. +__attribute__((weak)) void abort(void) { exit(127); } @@ -1410,9 +1414,11 @@ int at_quick_exit(AtexitFn fn) { // ---- getenv / system ---- // -// GS/OS has no environment. getenv always returns NULL. system -// always returns 0 (no command shell available). These exist to -// keep portable code compiling. +// Bare GS/OS has no environment. This WEAK getenv returns NULL; a GNO link +// (libcGno.o) overrides it with a shell-variable-table-backed getenv. system +// always returns 0 (no command shell available). These keep portable code +// compiling. +char *getenv(const char *name) __attribute__((weak)); char *getenv(const char *name) { (void)name; return (char *)0; } int system(const char *cmd) { (void)cmd; return 0; } @@ -2223,6 +2229,9 @@ typedef void (*__sighandler_t)(int); #define _NSIG 16 static __sighandler_t __sigHandlers[_NSIG]; +// Weak: a GNO link (libcGno.o) overrides with a kernel-backed signal() that +// routes real handlers through __sigTrampoline. +__attribute__((weak)) __sighandler_t signal(int sig, __sighandler_t handler) { if (sig < 0 || sig >= _NSIG) return _SIG_ERR; __sighandler_t prev = __sigHandlers[sig]; @@ -2231,6 +2240,7 @@ __sighandler_t signal(int sig, __sighandler_t handler) { return prev; } +__attribute__((weak)) int raise(int sig) { if (sig < 0 || sig >= _NSIG) return -1; __sighandler_t h = __sigHandlers[sig]; diff --git a/runtime/src/libcGno.c b/runtime/src/libcGno.c index 4dd450d..da6285e 100644 --- a/runtime/src/libcGno.c +++ b/runtime/src/libcGno.c @@ -12,6 +12,13 @@ #include #include +#include // strchr for the exec-family path/delimiter scans +#include // EINVAL / EINTR for the POSIX wrappers below +#include // SIGALRM for the sleep/usleep alarm dance +#include // struct tms for times() +#include // struct stat + S_IF*/S_IS* for stat/fstat/lstat/isatty +#include // pid_t/uid_t/gid_t + POSIX process/cred/jobctl prototypes +#include // O_* / SEEK_* + open()/creat() for the fd layer extern int main(int argc, char **argv); @@ -221,25 +228,27 @@ int __gnoStartup(const char *cmdline) { // CommonQuit reaps the process and returns control to the launcher (gsh). typedef struct { uint16_t pCount; } GnoQuitParm; -#define GSOS_QUIT 0x2029 +// _exit: GNO/ME program termination, carrying the exit STATUS to the parent. +// Routed through __gnoQuit (runtime/src/gnoGsos.s), not __gnoGsosCall, for two +// reasons: +// 1. Inline dispatch (not the stack form). GNO patches $E100A8 to OurGSOS, +// which reads the call number + pblock from the 6 inline bytes AFTER the +// jsl. The stack form leaves those bytes as whatever opcode follows, so +// OurGSOS mis-dispatches QUIT and the process stays restartable -- GNO then +// RE-RUNS main() (observed: main runs twice, silently draining redirected +// stdin on the first pass). +// 2. Status propagation. GNO's StackGSOS records the accumulator at the trap +// as the process exit code (ldx curProcInd; sta exitCode,x). __gnoGsosCall +// loads A = callNum right before dispatching, so every _exit recorded +// 0x2029 -> the parent's wait() decoded WEXITSTATUS = 0x29 (41). __gnoQuit +// leaves A = status (the call number is an inline constant), so wait() sees +// the real code. +extern __attribute__((noreturn)) void __gnoQuit(void *pBlock, int status); -// _exit: GNO/ME program termination via GS/OS QUIT, issued through the -// SAME inline GS/OS dispatch (__gnoGsosCall) as every other call here. -// -// Critical: this MUST use the inline form, not the stack-based form -// (`ldx #$2029 ; jsl $e100a8` with the pblock pushed). GNO patches -// $E100A8 to OurGSOS, which reads the call number + pblock from the 6 -// inline bytes AFTER the jsl. The stack form leaves those inline bytes -// as whatever opcode follows, so OurGSOS mis-dispatches the QUIT and the -// process is left restartable — GNO then RE-RUNS main() (observed: -// main executes twice, silently draining redirected stdin on the first -// pass). Routing QUIT through __gnoGsosCall (inline form + process-DP -// restore) terminates cleanly, so main() runs exactly once. __attribute__((noreturn)) void _exit(int status) { - (void)status; - GnoQuitParm q = { 0 }; // pCount=0 — plain QUIT to launcher - __gnoGsosCall(&q, GSOS_QUIT); // inline dispatch; does not return + GnoQuitParm q = { 0 }; // pCount=0 -- plain QUIT to launcher + __gnoQuit(&q, status); // inline dispatch; records status; no return for (;;) { } // unreachable } @@ -256,18 +265,87 @@ void _exit(int status) { extern int errno; // defined in libc.c -int fork(void) { +// gnoToPosixErrno: translate a GNO kernel errno into this runtime's +// (glibc) numbering. The K* process/stat/exec wrappers surface the kernel's RAW +// errno, and GNO numbers its errno space differently from us (tools/gno/src-repo/ +// include/sys/errno.h): GNO ENOENT is 4, EINVAL 6, EACCES 9, etc. -- versus our +// 2 / 22 / 13. Passing the raw value straight through makes a program's +// `errno == ENOENT` test misfire after stat()/access(). (The fd layer instead +// maps GS/OS TOOL errors via _mapErr; this maps the kernel's already-POSIX-shaped +// but differently-numbered errno.) Codes outside the shared table pass through. +static int gnoToPosixErrno(int e) { + switch (e) { + case 1: return EDOM; + case 2: return ERANGE; + case 3: return ENOMEM; + case 4: return ENOENT; + case 5: return EIO; + case 6: return EINVAL; + case 7: return EBADF; + case 8: return EMFILE; + case 9: return EACCES; + case 10: return EEXIST; + case 11: return ENOSPC; + case 12: return EPERM; + case 13: return ESRCH; + case 14: return EINTR; + case 15: return E2BIG; + case 16: return ENOEXEC; + case 17: return ECHILD; + case 18: return EAGAIN; + case 19: return ENOTDIR; + case 20: return ENOTTY; + case 21: return EPIPE; + case 22: return ESPIPE; + case 24: return EISDIR; + case 52: return EFAULT; + case 53: return ENODEV; + default: return 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.) +// +// `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. +int fork(void *subr) { int err = 0; - int r = Kfork(0, &err); - if (r < 0) errno = err; + int r = Kfork(subr, &err); + if (r < 0) errno = gnoToPosixErrno(err); return r; } +// 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 +// status through the pointer. +int wait(int *status) { + int err = 0; + int pid = Kwait((void *)status, &err); + if (pid < 0) errno = gnoToPosixErrno(err); + return pid; +} + + int kill(int pid, int sig) { int err = 0; int r = Kkill(pid, sig, &err); - if (r < 0) errno = err; + if (r < 0) errno = gnoToPosixErrno(err); return r; } @@ -280,7 +358,7 @@ int getpid(void) { int getppid(void) { int err = 0; int r = Kgetppid(&err); - if (r < 0) errno = err; + if (r < 0) errno = gnoToPosixErrno(err); return r; } @@ -288,7 +366,7 @@ int getppid(void) { int dup(int oldfd) { int err = 0; int r = Kdup(oldfd, &err); - if (r < 0) errno = err; + if (r < 0) errno = gnoToPosixErrno(err); return r; } @@ -296,7 +374,7 @@ int dup(int oldfd) { int dup2(int oldfd, int newfd) { int err = 0; int r = Kdup2(oldfd, newfd, &err); - if (r < 0) errno = err; + if (r < 0) errno = gnoToPosixErrno(err); return r; } @@ -304,7 +382,7 @@ int dup2(int oldfd, int newfd) { int pipe(int fds[2]) { int err = 0; int r = Kpipe(fds, &err); - if (r < 0) errno = err; + if (r < 0) errno = gnoToPosixErrno(err); return r; } @@ -313,3 +391,1278 @@ unsigned long alarm(unsigned long seconds) { int err = 0; return Kalarm((void *)seconds, &err); } + + +// waitpid: block until the specific child `pid` (or any child when pid == -1) +// is reaped, decoding its status into *status. GNO has no Kwaitpid, so this +// is a pure-C loop over wait() -- exactly GNO's own syscall.c waitpid. Two +// deviations from that source: WNOHANG cannot be honored (Kwait always +// blocks), so `options` is ignored rather than pretended-supported; and the +// process-group forms (pid < -1 or pid == 0) -- which GNO's libc abort()s on +// because getpgrp() is unimplemented -- are rejected with errno = EINVAL +// instead of aborting the process. +int waitpid(int pid, int *status, int options) { + int result; + (void)options; // WNOHANG unsupported: Kwait always blocks + if (pid < -1 || pid == 0) { + errno = EINVAL; // process-group waits not implemented + return -1; + } + for (;;) { + result = wait(status); + if (result == -1 || pid == -1 || pid == result) { + return result; // error, "any child", or the awaited pid + } + } +} + + +// SIGALRM catch-handler for sleep()/usleep(). The GNO kernel delivers the +// signal by building a context and JSLing here; we only flip a flag so the +// Ksigpause loop wakes, then return (RTL) to the kernel's context-restore. +// The handler's args are ignored, so the kernel's toolbox-style arg push vs. +// our C arg ABI does not matter -- only clean entry/return does. +static volatile int alarmRang; + + +static void alarmCatch(int sig, int code) { + alarmRang = 1; +} + + +// Shared body for sleep()/usleep(): install a SIGALRM catch, arm the kernel +// alarm (whole seconds via Kalarm, or tenths of a second via Kalarm10), then +// block in Ksigpause until it fires. Mirrors BSD/GNO sleep(3): SIGALRM is +// blocked around the arming so the alarm cannot fire before the pause commits, +// and Ksigpause atomically drops that block while it waits (race-free). The +// previous SIGALRM handler and signal mask are restored on the way out. +static void alarmSleep(unsigned long count, int tenths) { + int err = 0; + unsigned long alrmBit = 1UL << (SIGALRM - 1); // sigmask(SIGALRM) + unsigned long oldHandler; + unsigned long oldMask; + + oldHandler = Ksignal(SIGALRM, (void *)&alarmCatch, &err); + oldMask = Ksigblock((void *)alrmBit, &err); + alarmRang = 0; + if (tenths) { + Kalarm10((void *)count, &err); + } else { + Kalarm((void *)count, &err); + } + while (!alarmRang) { + Ksigpause((void *)(oldMask & ~alrmBit), &err); + } + Ksignal(SIGALRM, (void *)oldHandler, &err); + Ksigsetmask((void *)oldMask, &err); +} + + +// sleep: suspend the process for `seconds` whole seconds using the kernel's +// second-granularity alarm. Returns 0 (we always sleep the full interval; +// there is no early-wake path since only our own SIGALRM is unblocked). +unsigned int sleep(unsigned int seconds) { + if (seconds == 0) { + return 0; + } + alarmSleep((unsigned long)seconds, 0); + return 0; +} + + +// usleep: suspend for `useconds` microseconds, floored at the kernel's alarm +// granularity of one tenth of a second (Kalarm10). Any nonzero request thus +// sleeps at least 0.1 s. Returns 0. +int usleep(unsigned long useconds) { + unsigned long tenths; + if (useconds == 0) { + return 0; + } + tenths = (useconds + 99999UL) / 100000UL; // 100000 us == 1 tenth + if (tenths == 0) { + tenths = 1; // floor: never round down to 0 + } + alarmSleep(tenths, 1); + return 0; +} + + +// pause: block until any signal is delivered. Ksigpause(0) unblocks all +// signals and waits; on wake POSIX pause always reports failure with EINTR. +int pause(void) { + int err = 0; + Ksigpause((void *)0, &err); + errno = EINTR; + return -1; +} + + +// times: fill *buf with this process' CPU-time accounting. Thin wrapper over +// the GNO kernel Ktimes primitive, which writes tms_utime/tms_cutime (60 Hz +// ticks) and zeroes tms_stime/tms_cstime, returning 0. Ktimes has retSize=4 +// but the kernel only returns a 16-bit 0; the wrapper zero-fills the hi result +// word (pea 0), so the clock_t result is a clean 0. +clock_t times(struct tms *buf) { + int err = 0; + return (clock_t)Ktimes(buf, &err); +} + + +// ---- file status: stat/fstat/lstat + isatty (punch-list #8) --------- +// Kstat/Kfstat/Klstat (toolset $03 funcs 0x2703/0x2803/0x2903) fill a struct +// stat directly and return an ALREADY-POSIX errno through their pointer arg +// (the kernel owns the tool-error->errno map), so these are thin wrappers. +// struct stat MUST byte-match kern/gno/stat.c -- pinned by the _Static_assert +// in . (NOTE: like every other K* wrapper here, errno gets the +// kernel's errno-space value, which differs from runtime/include/errno.h -- +// a pre-existing project-wide inconsistency, not reconciled per-call.) + + +// fstat: status of the open descriptor fd. POSIX 0/1/2 are remapped to GNO's +// 1-based table via gnoFd (same as read/write) -- the kernel rejects the +// unmapped fd 0 as EBADF, so the remap is load-bearing, not cosmetic. +int fstat(int fd, struct stat *sb) { + int err = 0; + int r = Kfstat((int)gnoFd(fd), sb, &err); + if (r < 0) { + errno = gnoToPosixErrno(err); + } + return r; +} + + +// isatty: true when fd is a terminal. POSIX does not require ioctl -- a GNO +// TTY descriptor stats as a character special, so the predicate is S_ISCHR. +int isatty(int fd) { + struct stat sb; + if (fstat(fd, &sb) != 0) { + return 0; // errno set by fstat (EBADF) + } + if (S_ISCHR(sb.st_mode)) { + return 1; + } + errno = ENOTTY; + return 0; +} + + +// lstat: like stat but does not follow a final symlink. GNO has no real +// symlinks, so Klstat behaves identically to Kstat; it takes a plain C string +// (the kernel does the C->GS/OS conversion + path expansion). +int lstat(const char *path, struct stat *sb) { + int err = 0; + int r = Klstat((void *)path, sb, &err); + if (r < 0) { + errno = gnoToPosixErrno(err); + } + return r; +} + + +// stat: status of the file named by path. Kstat takes a plain C string. +int stat(const char *path, struct stat *sb) { + int err = 0; + int r = Kstat((void *)path, sb, &err); + if (r < 0) { + errno = gnoToPosixErrno(err); + } + return r; +} + + +// ---- uid/gid + job control (punch-list #11) ------------------------- +// Thin wrappers over the toolset-$03 credential / process-group primitives +// (K* in gno/kernel.h), mirroring GNO's own libc trap.asm / syscall.c. +// +// The getid family (getuid/geteuid/getgid/getegid) NEVER fails in the kernel: +// the trailing *errno arg is written only on an (unreachable) error path, and +// GNO's own trap.asm stubs return the raw result without inspecting it. So +// these do NOT gate on r < 0. The setters and the pgrp calls DO carry a real +// error path and stash *errno into the global errno on -1. + + +// getegid: effective group id of the calling process. Cannot fail. +gid_t getegid(void) { + int err = 0; + return (gid_t)Kgetegid(&err); +} + + +// geteuid: effective user id of the calling process. Cannot fail. +uid_t geteuid(void) { + int err = 0; + return (uid_t)Kgeteuid(&err); +} + + +// getgid: real group id of the calling process. Cannot fail. +gid_t getgid(void) { + int err = 0; + return (gid_t)Kgetgid(&err); +} + + +// getpgrp: process group of the calling process. GNO has no zero-arg +// primitive -- it is K_getpgrp(getpid()), exactly like GNO's own getpgrp() +// (syscall.c: _getpgrp(getpid())). Returns the pgrp, or -1 with errno set. +pid_t getpgrp(void) { + int err = 0; + int r = K_getpgrp(getpid(), &err); + if (r < 0) { + errno = gnoToPosixErrno(err); + } + return r; +} + + +// getuid: real user id of the calling process. Cannot fail. +uid_t getuid(void) { + int err = 0; + return (uid_t)Kgetuid(&err); +} + + +// setgid: set the real (and, for the superuser, effective) group id. +int setgid(gid_t gid) { + int err = 0; + int r = Ksetgid((int)gid, &err); + if (r < 0) { + errno = gnoToPosixErrno(err); + } + return r; +} + + +// setpgid: place process pid into process group pgrp. Maps to Ksetpgrp(pid, +// pgrp) -- note the (pid, pgrp) order, matching trap.asm's 0x3403 entry. +int setpgid(pid_t pid, pid_t pgrp) { + int err = 0; + int r = Ksetpgrp(pid, pgrp, &err); + if (r < 0) { + errno = gnoToPosixErrno(err); + } + return r; +} + + +// setpgrp: BSD spelling of setpgid; shares the same kernel entry (0x3403). +int setpgrp(pid_t pid, pid_t pgrp) { + return setpgid(pid, pgrp); +} + + +// setsid: create a new session and become its leader. GNO exposes no session +// primitive (tcnewpgrp/0x1803 only reassigns a tty foreground group), so this +// stubs ENOSYS. +pid_t setsid(void) { + errno = ENOSYS; + return -1; +} + + +// setuid: set the real (and, for the superuser, effective) user id. +int setuid(uid_t uid) { + int err = 0; + int r = Ksetuid((int)uid, &err); + if (r < 0) { + errno = gnoToPosixErrno(err); + } + return r; +} + + +// tcgetpgrp: foreground process group of the terminal behind fd. GNO's tty +// driver routes both TIOCGPGRP and TIOCSPGRP to its `invalid` handler +// (kern/gno/tty.asm), so the query can never succeed -- stub ENOSYS rather +// than issue an ioctl guaranteed to fail with a misleading errno. +pid_t tcgetpgrp(int fd) { + (void)fd; + errno = ENOSYS; + return -1; +} + + +// tcsetpgrp: make pgrp the foreground process group of the terminal behind fd. +// Maps to Ktctpgrp(gnoFd(fd), pgrp) (0x1A03); fd is a POSIX 0/1/2 std-stream +// descriptor, so it goes through gnoFd like every other tty-fd call here. +int tcsetpgrp(int fd, pid_t pgrp) { + int err = 0; + int r = Ktctpgrp((int)gnoFd(fd), pgrp, &err); + if (r < 0) { + errno = gnoToPosixErrno(err); + } + return r; +} + + +// ---- environment: GNO shell-variable table (punch-list #6) ---------- +// GNO has no Unix environ[]; the "environment" is the kernel SHELL-VARIABLE +// table, reached through GS/OS Shell calls dispatched by the SAME inline +// $E100A8 mechanism as the file calls (__gnoGsosCall) -- zero new asm. +// Layouts + call numbers mirror GNO libc's lib/libc/stdlib/environ.c. + +// GS/OS class-0 shell-call numbers (from GNO environ.c usage). +#define SHELL_SETGS 0x0146 // SetGS: set (and export) a variable +#define SHELL_READVARIABLEGS 0x014B // ReadVariableGS: fetch a value +#define SHELL_UNSETVARIABLEGS 0x0155 // UnsetVariableGS: delete a variable + +// GS/OS length-prefixed strings (Word length; char text[]). The shell's +// GSString255 text is 255 bytes; our copy carries one extra byte so getenv +// can NUL-terminate the value in place without overflowing the shell's region. +#define GSSTR255_TEXT 255 +#define GSSTR255_SIZE (2 + GSSTR255_TEXT) // 257 = shell sizeof(GSString255) +typedef struct { uint16_t length; char text[GSSTR255_TEXT + 1]; } GnoGSString255; +typedef struct { uint16_t bufSize; GnoGSString255 bufString; } GnoResultBuf255; + +// Shell param blocks (field order per GNO's shell interface; confirmed by +// environ.c's field accesses: pCount, name, value, export). +typedef struct { uint16_t pCount; GnoGSString255 *name; GnoGSString255 *value; uint16_t export; } GnoSetParm; +typedef struct { uint16_t pCount; GnoGSString255 *name; GnoResultBuf255 *value; uint16_t export; } GnoReadVarParm; +typedef struct { uint16_t pCount; GnoGSString255 *name; } GnoUnsetVarParm; + +_Static_assert(sizeof(void *) == 4, "GS/OS pblock pointers must be 4 bytes"); +_Static_assert(__builtin_offsetof(GnoResultBuf255, bufString.length) == 2, "GSString length must sit at ResultBuf+2"); +_Static_assert(__builtin_offsetof(GnoResultBuf255, bufString.text) == 4, "GSString text must sit at ResultBuf+4"); + +// POSIX environ[] mirror. This thin port keeps only the internal (shell) +// table, so environ stays NULL/lazy: use the getenv/setenv accessors. +char **environ = 0; + +// Shared GS/OS-string scratch for the env accessors -- single-threaded, so one +// name + one value buffer serve getenv/setenv/unsetenv. getenv's result must +// persist across the call (the returned pointer aims into it), so it is its own +// buffer. setenv fills envName only AFTER its internal getenv overwrite-check, +// so sharing envName between them is safe. (Consolidated from five per-function +// static buffers to keep the GNO libc's bank-0 BSS footprint small.) +static GnoGSString255 envName; // name arg (getenv/setenv/unsetenv) +static GnoGSString255 envSetValue; // setenv value arg +static GnoResultBuf255 envResult; // getenv result (returned-pointer target) + +int setenv(const char *name, const char *value, int overwrite); // putenv calls it + + +// Copy up to n bytes of str into a GS/OS length-prefixed string, clamping to +// the shell's 255-byte text capacity. +static void gnoFillGSStringN(GnoGSString255 *s, const char *str, uint16_t n) { + uint16_t i; + if (n > GSSTR255_TEXT) { + n = GSSTR255_TEXT; + } + i = 0; + while (i < n) { + s->text[i] = str[i]; + i++; + } + s->length = n; +} + + +// strlen returning a 16-bit count (shell names/values never exceed 255). +static uint16_t gnoEnvLen(const char *s) { + uint16_t n; + n = 0; + while (s[n] != 0) { + n++; + } + return n; +} + + +// getenv: read shell variable name via ReadVariableGS ($014B). Returns a +// pointer into a STATIC result buffer (NUL-terminated in place), or NULL if +// the variable is unset, not exported, or the call errors. A trailing '=' on +// name is stripped. The static buffer is overwritten by the next getenv call. +char *getenv(const char *name) { + GnoReadVarParm pb; + uint16_t nameLen; + uint16_t length; + + if (name == 0) { + return 0; + } + nameLen = gnoEnvLen(name); + if (nameLen != 0 && name[nameLen - 1] == '=') { + nameLen--; + } + if (nameLen == 0) { + return 0; + } + gnoFillGSStringN(&envName, name, nameLen); + + pb.pCount = 3; + pb.name = &envName; + envResult.bufSize = GSSTR255_SIZE; + pb.value = &envResult; + pb.export = 0; + if (__gnoGsosCall(&pb, SHELL_READVARIABLEGS) != 0) { + return 0; + } + if (pb.export == 0) { + return 0; // not defined / not exported + } + length = envResult.bufString.length; + if (length == 0) { + return 0; + } + if (length > GSSTR255_TEXT) { + length = GSSTR255_TEXT; + } + envResult.bufString.text[length] = 0; + return envResult.bufString.text; +} + + +// putenv: split "NAME=value" and forward to setenv(name, value, 1). The +// caller's string is not retained (GNO keeps only the shell table). +int putenv(const char *str) { + char nameBuf[GSSTR255_TEXT + 1]; + uint16_t i; + + if (str == 0) { + return -1; + } + i = 0; + while (str[i] != 0 && str[i] != '=' && i < GSSTR255_TEXT) { + nameBuf[i] = str[i]; + i++; + } + if (str[i] != '=') { + return -1; // no '=' -> not NAME=value + } + nameBuf[i] = 0; + return setenv(nameBuf, str + i + 1, 1); +} + + +// setenv: set shell variable name to value via SetGS ($0146) with export=1. +// Honors overwrite: when zero and the variable exists, leaves it unchanged. +int setenv(const char *name, const char *value, int overwrite) { + GnoSetParm pb; + uint16_t nameLen; + + if (name == 0 || value == 0) { + errno = EINVAL; + return -1; + } + if (*value == '=') { + value++; + } + nameLen = gnoEnvLen(name); + if (nameLen != 0 && name[nameLen - 1] == '=') { + nameLen--; + } + if (nameLen == 0 || *value == 0) { + errno = EINVAL; + return -1; + } + if (!overwrite && getenv(name) != 0) { + return 0; // exists and not overwriting (uses envName) + } + gnoFillGSStringN(&envName, name, nameLen); // re-filled AFTER the getenv check + gnoFillGSStringN(&envSetValue, value, gnoEnvLen(value)); + + pb.pCount = 3; + pb.name = &envName; + pb.value = &envSetValue; + pb.export = 1; + if (__gnoGsosCall(&pb, SHELL_SETGS) != 0) { + errno = ENOMEM; + return -1; + } + return 0; +} + + +// unsetenv: delete shell variable name via UnsetVariableGS ($0155). +int unsetenv(const char *name) { + GnoUnsetVarParm pb; + uint16_t nameLen; + + if (name == 0) { + errno = EINVAL; + return -1; + } + nameLen = gnoEnvLen(name); + if (nameLen != 0 && name[nameLen - 1] == '=') { + nameLen--; + } + gnoFillGSStringN(&envName, name, nameLen); + + pb.pCount = 1; + pb.name = &envName; + __gnoGsosCall(&pb, SHELL_UNSETVARIABLEGS); + return 0; +} + + +// ---- POSIX fd layer: open/close/lseek/creat/unlink (punch-list #9) --- +// GS/OS-toolbox emulation over the gsos* wrappers above -- NOT a K* job. +// (rename() already lives in libc.c as a strong symbol; do NOT add one here.) + +// GS/OS Open requestAccess values (GS/OS Reference, requestAccess field). +#define GSOS_READ_ENABLE 0x0001 +#define GSOS_WRITE_ENABLE 0x0002 +#define GSOS_RW_ENABLE 0x0003 +// Create access for a fresh data file: destroy|rename|backup|write|read enabled. +#define GSOS_CREATE_ACCESS 0x00C3 +#define GSOS_FILETYPE_TXT 0x0004 +#define GSOS_FILETYPE_BIN 0x0006 +#define GSOS_STORAGE_STD 0x0001 // standard (seedling/sapling/tree) file +#define GSOS_PATH_MAX 256 + +// Class-1 SetMark/SetEOF position block: base + displacement. GetMark/GetEOF +// return only a bare position, so the Get side and truncate-to-0 keep using the +// 3-field GnoMarkRec/GnoEOFRec; positioned seeks need this 4-field form (matches +// GNO's own lib/libc/sys/syscall.c lseek, which uses pCount=3 base=0). +typedef struct { + uint16_t pCount; + uint16_t refNum; + uint16_t base; + unsigned long displacement; +} GnoSetPosParm; + +// Class-1 GSString (length-prefixed pathname). libc.c's __fillGSString is +// static, so the fd layer carries its own builder + buffer. +typedef struct { + uint16_t length; + char text[GSOS_PATH_MAX]; +} GsStringT; +static GsStringT gsPath; + + +// _mapErr: translate a GS/OS tool error into a POSIX errno. Ported from GNO's +// kernel _mapErr (kern/gno/stat.c): errors GNO already folded into the 0x43xx +// space carry the POSIX code in their low byte; the rest map by GS/OS error +// number. 0 stays 0. +static int _mapErr(uint16_t gsErr) { + if (gsErr == 0) { + return 0; + } + if ((gsErr & 0xFF00) == 0x4300) { + return (int)(gsErr & 0x00FF); + } + switch (gsErr) { + case 0x43: + return EBADF; + case 0x44: + case 0x45: + case 0x46: + return ENOENT; + case 0x47: + case 0x50: + return EEXIST; + case 0x48: + case 0x49: + return ENOSPC; + case 0x4A: + return ENOTDIR; + case 0x4B: + case 0x4F: + case 0x53: + return EINVAL; + case 0x4E: + return EACCES; + case 0x54: + return ENOMEM; + default: + return EIO; + } +} + + +// buildGsString: fill a class-1 GSString (uint16 length + text) from a NUL- +// terminated C path. Returns -1 if the path is longer than GSOS_PATH_MAX. +static int buildGsString(GsStringT *dst, const char *path) { + uint16_t n; + + n = 0; + while (path[n] != 0 && n < GSOS_PATH_MAX) { + dst->text[n] = path[n]; + n++; + } + if (path[n] != 0) { + return -1; + } + dst->length = n; + return 0; +} + + +// close: POSIX fd close over GS/OS Close. fd is fed through gnoFd() so the +// inherited console streams (0/1/2 -> GNO refNums 1/2/3) still close cleanly; +// an open()-returned refNum (>= 4) passes through unchanged. +int close(int fd) { + GnoRefNumRec rec; + int perr; + + rec.pCount = 1; + rec.refNum = gnoFd(fd); + perr = _mapErr(gsosClose(&rec)); + if (perr != 0) { + errno = perr; + return -1; + } + return 0; +} + + +// creat: create-or-truncate a file for writing. POSIX defines it as this +// open() call; mode is accepted for source compatibility but ignored (GS/OS +// create access is fixed full-access). +int creat(const char *path, mode_t mode) { + (void)mode; + return open(path, O_CREAT | O_TRUNC | O_WRONLY, 0); +} + + +// lseek: reposition an open fd's mark over GS/OS GetEOF/GetMark/SetMark. +// SEEK_SET/CUR/END resolve to an absolute target; seeking past EOF extends the +// file first (GS/OS cannot park the mark beyond EOF). fd is fed through +// gnoFd(). Returns the new absolute offset, or -1 with errno set. +off_t lseek(int fd, off_t offset, int whence) { + GnoEOFRec eof; + GnoMarkRec mark; + GnoSetPosParm sp; + uint16_t rn; + off_t target; + int perr; + + rn = gnoFd(fd); + eof.pCount = 2; + eof.refNum = rn; + eof.val = 0; + perr = _mapErr(gsosGetEOF(&eof)); + if (perr != 0) { + errno = perr; + return -1; + } + switch (whence) { + case SEEK_SET: + target = offset; + break; + case SEEK_CUR: + mark.pCount = 2; + mark.refNum = rn; + mark.val = 0; + perr = _mapErr(gsosGetMark(&mark)); + if (perr != 0) { + errno = perr; + return -1; + } + target = (off_t)mark.val + offset; + break; + case SEEK_END: + target = (off_t)eof.val + offset; + break; + default: + errno = EINVAL; + return -1; + } + if (target < 0) { + errno = EINVAL; + return -1; + } + sp.pCount = 3; + sp.refNum = rn; + sp.base = 0; // 0 = displacement measured from start of file + sp.displacement = (unsigned long)target; + if ((unsigned long)target > eof.val) { + perr = _mapErr(__gnoGsosCall(&sp, GSOS_SETEOF)); + if (perr != 0) { + errno = perr; + return -1; + } + } + perr = _mapErr(__gnoGsosCall(&sp, GSOS_SETMARK)); + if (perr != 0) { + errno = perr; + return -1; + } + return target; +} + + +// open: POSIX open(2) over GS/OS Open (+ Create for O_CREAT). The access mode +// selects the GS/OS requestAccess; O_CREAT makes a missing file; O_EXCL fails +// an existing file; O_TRUNC resets EOF to 0; O_APPEND parks the mark at EOF. +// Returns the RAW GS/OS refNum (always >= 4, never colliding with the 1/2/3 +// console fds), or -1 with errno set. mode is variadic + unused. +int open(const char *path, int flags, ...) { + GnoOpenParm op; + GnoCreateParm cp; + GnoEOFRec eof; + GnoRefNumRec rec; + uint16_t gsErr; + int perr; + int accMode; + + if (buildGsString(&gsPath, path) != 0) { + errno = ENAMETOOLONG; + return -1; + } + accMode = flags & O_ACCMODE; + op.pCount = 3; + op.refNum = 0; + op.pathname = &gsPath; + if (accMode == O_WRONLY) { + op.requestAccess = GSOS_WRITE_ENABLE; + } else if (accMode == O_RDWR) { + op.requestAccess = GSOS_RW_ENABLE; + } else { + op.requestAccess = GSOS_READ_ENABLE; + } + + gsErr = gsosOpen(&op); + perr = _mapErr(gsErr); + + if (gsErr == 0 && (flags & O_CREAT) && (flags & O_EXCL)) { + // O_CREAT|O_EXCL and the file already exists -- fail after closing it. + rec.pCount = 1; + rec.refNum = op.refNum; + gsosClose(&rec); + errno = EEXIST; + return -1; + } + if (perr == ENOENT && (flags & O_CREAT)) { + // Missing file -- create it, then reopen. + cp.pCount = 5; + cp.pathname = &gsPath; + cp.access = GSOS_CREATE_ACCESS; + cp.fileType = (flags & O_BINARY) ? GSOS_FILETYPE_BIN : GSOS_FILETYPE_TXT; + cp.auxType = 0; + cp.storageType = GSOS_STORAGE_STD; + perr = _mapErr(gsosCreate(&cp)); + if (perr != 0) { + errno = perr; + return -1; + } + perr = _mapErr(gsosOpen(&op)); + if (perr != 0) { + errno = perr; + return -1; + } + } else if (perr != 0) { + errno = perr; + return -1; + } + + // File is open (refNum in op.refNum). Apply size/position modifiers for + // writable opens only. + if (accMode != O_RDONLY) { + if (flags & O_TRUNC) { + eof.pCount = 2; + eof.refNum = op.refNum; + eof.val = 0; + gsosSetEOF(&eof); + } + if (flags & O_APPEND) { + eof.pCount = 2; + eof.refNum = op.refNum; + eof.val = 0; + if (gsosGetEOF(&eof) == 0) { + GnoSetPosParm sp; + sp.pCount = 3; + sp.refNum = op.refNum; + sp.base = 0; + sp.displacement = eof.val; + __gnoGsosCall(&sp, GSOS_SETMARK); + } + } + } + return (int)op.refNum; +} + + +// unlink: remove a file via GS/OS Destroy. Returns 0, or -1 with errno set. +int unlink(const char *path) { + GnoDestroyParm dp; + int perr; + + if (buildGsString(&gsPath, path) != 0) { + errno = ENAMETOOLONG; + return -1; + } + dp.pCount = 1; + dp.pathname = &gsPath; + perr = _mapErr(gsosDestroy(&dp)); + if (perr != 0) { + errno = perr; + return -1; + } + return 0; +} + + +// ---- exec family + access (punch-list #5 + #7) ---------------------- +// GNO's kernel exec (K_execve, $1D03) is NOT the Unix argv/envp exec: it takes +// a FILE path and ONE flattened command-line string, and the kernel prepends +// its own 8-byte "BYTEWRKS" tag (which __gnoStartup already skips), so we must +// NOT add it here. These port lib/libc/sys/exec.c: gnoBuildCmd space-joins +// argv (stripping argv[0]'s GS/OS prefix), gnoBuildPath does the $PATH search +// for execvp/execlp, and K_execve carries file + cmd. On success K_execve +// replaces the process image and never returns. envp is ignored (GNO exec has +// no environment vector -- see the punch-list notes). + +// va_list via compiler builtins (there is no in runtime/include; +// snprintf.c does the same). execl/execlp/execle need it. +typedef __builtin_va_list execVaList; +#define EXEC_VA_START(ap, last) __builtin_va_start(ap, last) +#define EXEC_VA_ARG(ap, ty) __builtin_va_arg(ap, ty) +#define EXEC_VA_END(ap) __builtin_va_end(ap) + +#define EXEC_CMD_MAX 256 // flattened cmd / candidate-path cap +#define EXEC_ARGV_MAX 32 // execl* argv gather cap (excess dropped) +#define GNO_DEFAULT_PATH "/bin /usr/bin" // $PATH fallback (the set gsh searches) + +// buildCmd/buildPath output buffers. exec never returns on success and each +// result is consumed immediately by K_execve, so static single-use is safe. +static char execCmdBuf[EXEC_CMD_MAX]; +static char execPathBuf[EXEC_CMD_MAX]; +// Allowable first chars of a fully-specified (root) path name (from exec.c). +static const char *pfxBrkStr = "/:0123456789.*@"; + + +// access: existence/permission test over stat(). Only F_OK (existence) is +// meaningfully supported -- GNO stat reports no faithful owner/perm bits, so +// R/W/X_OK degrade to an existence test, which is exactly what execvp's $PATH +// search needs. Returns 0 if the path stats, else -1 with errno set. +int access(const char *path, int mode) { + struct stat sb; + (void)mode; + if (path == 0 || *path == 0) { + errno = ENOENT; + return -1; + } + return (stat(path, &sb) == 0) ? 0 : -1; // stat() set errno on failure +} + + +// isRootPath: TRUE if name is a fully-specified path from the file-system root +// (leading '/', ':', a digit, or a '.'/'*'/'@' prefix form) and thus needs no +// $PATH search. Ported from exec.c isRootPath (with an empty-string guard). +static int isRootPath(const char *name) { + const char *p; + if (*name == 0) { + return 0; + } + p = strchr(pfxBrkStr, *name); + if (p == 0) { + return 0; + } + switch (*p) { + case '/': + case ':': + if (*(p + 1) == 0) { + return 0; // zero-length volume/prefix not allowed + } + return 1; + case '.': + switch (*(p + 1)) { + case 0: + case '/': + case ':': + return 0; // "." / "./" / ".:" are not root paths + default: + return 1; + } + case '*': + case '@': + switch (*(p + 1)) { + case 0: + case '/': + case ':': + return 1; + default: + return 0; + } + default: + return 1; // begins with a digit + } +} + + +// gnoBuildCmd: flatten argv[] into ONE space-joined command line in execCmdBuf, +// stripping argv[0]'s GS/OS prefix to the leaf after the last '/' or ':' +// (matching exec.c buildCmd). Returns execCmdBuf, or NULL with errno=E2BIG on +// overflow. An empty argv yields a zero-length string. +static char *gnoBuildCmd(char *const *argv) { + uint16_t pos; + int i; + const char *s; + char delim; + + pos = 0; + execCmdBuf[0] = 0; + if (argv == 0 || argv[0] == 0) { + return execCmdBuf; // zero-length command line + } + + // Strip argv[0]'s prefix: keep only the leaf after the last delimiter. + delim = (strchr(argv[0], ':') == 0) ? '/' : ':'; + s = argv[0]; + while (*s != 0) { + s++; + } + while (s > argv[0] && *s != delim) { + s--; + } + if (*s == delim) { + s++; + } + + // Append the leaf, then argv[1..] each preceded by a single space. + i = 0; + while (1) { + while (*s != 0) { + if (pos >= EXEC_CMD_MAX - 1) { + errno = E2BIG; + return 0; + } + execCmdBuf[pos++] = *s++; + } + i++; + if (argv[i] == 0) { + break; + } + if (pos >= EXEC_CMD_MAX - 1) { + errno = E2BIG; + return 0; + } + execCmdBuf[pos++] = ' '; + s = argv[i]; + } + execCmdBuf[pos] = 0; + return execCmdBuf; +} + + +// gnoBuildPath: resolve a bare command name to a full path via $PATH (for +// execvp/execlp). A rooted/prefixed name (isRootPath) is returned verbatim. +// Otherwise each space-separated $PATH directory (getenv("PATH"), or +// GNO_DEFAULT_PATH when unset) is tried as and probed with +// access(F_OK); the first hit wins. Returns execPathBuf, or NULL with +// errno=ENOENT. +static char *gnoBuildPath(const char *file) { + const char *pathEnv; + const char *p; + const char *f; + char delim; + uint16_t pos; + + if (isRootPath(file)) { + pos = 0; + while (file[pos] != 0 && pos < EXEC_CMD_MAX - 1) { + execPathBuf[pos] = file[pos]; + pos++; + } + execPathBuf[pos] = 0; + return execPathBuf; + } + + pathEnv = getenv("PATH"); + if (pathEnv == 0 || *pathEnv == 0) { + pathEnv = GNO_DEFAULT_PATH; + } + delim = (strchr(pathEnv, '/') == 0) ? ':' : '/'; + + p = pathEnv; + while (*p != 0) { + while (*p == ' ') { + p++; + } + if (*p == 0) { + break; + } + // Copy one directory token. + pos = 0; + while (*p != 0 && *p != ' ' && pos < EXEC_CMD_MAX - 1) { + execPathBuf[pos++] = *p++; + } + // Resync past any tail of an over-long token. + while (*p != 0 && *p != ' ') { + p++; + } + // Join with the delimiter unless the token already ends in one. + if (pos > 0 && execPathBuf[pos - 1] != delim && pos < EXEC_CMD_MAX - 1) { + execPathBuf[pos++] = delim; + } + // Append the file name. + f = file; + while (*f != 0 && pos < EXEC_CMD_MAX - 1) { + execPathBuf[pos++] = *f++; + } + execPathBuf[pos] = 0; + if (access(execPathBuf, F_OK) == 0) { + return execPathBuf; // found it + } + } + errno = ENOENT; + return 0; +} + + +// execCore: shared tail for execv/execve/execvp. K_execve only returns on +// failure (on success the process image is replaced), so this always ends by +// surfacing the kernel errno and returning -1. +static int execCore(const char *path, const char *cmd) { + int err = 0; + K_execve((void *)path, (void *)cmd, &err); + errno = gnoToPosixErrno(err); // reached only if the exec failed + return -1; +} + + +// execve: flatten argv into a command line and hand file+cmd to K_execve. +// envp is accepted for signature compatibility but ignored (GNO exec has no +// environment vector). Never returns on success; -1/errno on failure. +int execve(const char *path, char *const *argv, char *const *envp) { + char *cmd; + (void)envp; + cmd = gnoBuildCmd(argv); + if (cmd == 0) { + return -1; // errno = E2BIG from gnoBuildCmd + } + return execCore(path, cmd); +} + + +// execv: execve without an environment (GNO ignores envp regardless). +int execv(const char *path, char *const *argv) { + return execve(path, argv, 0); +} + + +// execvp: like execv but resolve file through $PATH first (when not rooted). +int execvp(const char *file, char *const *argv) { + char *path; + char *cmd; + path = gnoBuildPath(file); + if (path == 0) { + return -1; // errno = ENOENT from gnoBuildPath + } + cmd = gnoBuildCmd(argv); + if (cmd == 0) { + return -1; + } + return execCore(path, cmd); +} + + +// execl: variadic form of execv. Gather arg, ... (NULL-terminated) into a +// local argv[] (capped at EXEC_ARGV_MAX) and defer to execv. +int execl(const char *path, const char *arg, ...) { + char *argv[EXEC_ARGV_MAX]; + execVaList ap; + int i; + + argv[0] = (char *)arg; + i = 1; + if (arg != 0) { + EXEC_VA_START(ap, arg); + while (i < EXEC_ARGV_MAX - 1) { + argv[i] = EXEC_VA_ARG(ap, char *); + if (argv[i] == 0) { + break; + } + i++; + } + EXEC_VA_END(ap); + } + argv[i] = 0; + return execv(path, argv); +} + + +// execlp: variadic form of execvp (PATH-searched). +int execlp(const char *file, const char *arg, ...) { + char *argv[EXEC_ARGV_MAX]; + execVaList ap; + int i; + + argv[0] = (char *)arg; + i = 1; + if (arg != 0) { + EXEC_VA_START(ap, arg); + while (i < EXEC_ARGV_MAX - 1) { + argv[i] = EXEC_VA_ARG(ap, char *); + if (argv[i] == 0) { + break; + } + i++; + } + EXEC_VA_END(ap); + } + argv[i] = 0; + return execvp(file, argv); +} + + +// execle: variadic form of execve. The trailing envp (after the NULL argv +// terminator) is ignored -- GNO exec has no environment vector. +int execle(const char *path, const char *arg, ...) { + char *argv[EXEC_ARGV_MAX]; + execVaList ap; + int i; + + argv[0] = (char *)arg; + i = 1; + if (arg != 0) { + EXEC_VA_START(ap, arg); + while (i < EXEC_ARGV_MAX - 1) { + argv[i] = EXEC_VA_ARG(ap, char *); + if (argv[i] == 0) { + break; + } + i++; + } + EXEC_VA_END(ap); + } + argv[i] = 0; + return execve(path, argv, 0); // trailing envp intentionally ignored +} + + +// ---- signals (punch-list #10) --------------------------------------- +// The GNO kernel calls an installed handler in ORCA/C convention (args on the +// stack, callee-pops, RTL to the kernel's context-restore), which does not +// match our arg0-in-A / caller-pops ABI. __sigTrampoline (gnoGsos.s) is +// registered as the kernel handler for every CAUGHT signal: it reads the signal +// number off the delivery frame, switches to this process's DP/DBR, dispatches +// the real C handler from userHandlers[] (signal number in A), then does the +// ORCA callee-pop return the kernel's ctxtRestore expects. __gnoCaptureDP +// records this process's direct page for the trampoline. +extern void __sigTrampoline(void); // asm: kernel-side signal-handler shim +extern void __gnoCaptureDP(void); // asm: stash our DP -> __gnoProcDP + +// Real C handlers, indexed by signal number (1..NSIG-1; slot 0 unused). Read +// by name from __sigTrampoline, so it must be a non-static global; KEEP so LTO +// cannot DCE the table the asm references. +KEEP void (*userHandlers[NSIG])(int); + +// Shadow of the user-visible disposition (incl SIG_DFL/SIG_IGN) so signal()/ +// sigaction() can report the previous handler. Zero-init == SIG_DFL. +static sig_t currentHandler[NSIG]; + + +// abort: raise SIGABRT, then terminate with the conventional 128+signal status. +// libc.c's abort() is weak; this GNO one wins. +__attribute__((noreturn)) +void abort(void) { + raise(SIGABRT); + _exit(128 + SIGABRT); // 134; only reached if the handler returned + for (;;) { + } +} + + +// raise: send a signal to the calling process. POSIX raise(sig) == kill( +// getpid(), sig); the self-signal path runs the handler synchronously inside +// the kernel, coming back through __sigTrampoline in the kernel's DP context. +int raise(int sig) { + return kill(getpid(), sig); +} + + +// sigaction: install/query a handler. GNO has no distinct sigaction primitive +// (only Ksignal), so this is a thin layer over signal(): sa_mask/sa_flags are +// accepted but not honored. act==NULL only reports the current disposition. +int sigaction(int sig, const struct sigaction *act, struct sigaction *oact) { + sig_t old; + if (sig < 1 || sig >= NSIG) { + errno = EINVAL; + return -1; + } + old = currentHandler[sig]; + if (act != 0) { + if (signal(sig, (sig_t)act->sa_handler) == SIG_ERR) { + return -1; + } + } + if (oact != 0) { + oact->sa_handler = (void (*)(int))old; + oact->sa_mask = 0; + oact->sa_flags = 0; + } + return 0; +} + + +// sigblock: BSD -- add mask to the blocked set, return the previous mask. +unsigned long sigblock(unsigned long mask) { + int err = 0; + return Ksigblock((void *)mask, &err); +} + + +// signal: install func as the disposition for sig. SIG_DFL/SIG_IGN are handed +// straight to the kernel (no callback, no ABI mismatch). A real handler is +// stored in userHandlers[] and __sigTrampoline is what the kernel calls -- it +// adapts the kernel's ORCA/C callee-pops frame to our arg0-in-A handler. We +// snapshot our process DP first (the trampoline needs it for the self-signal +// path). Returns the PREVIOUS user-visible disposition. +sig_t signal(int sig, sig_t func) { + int err = 0; + sig_t old; + if (sig < 1 || sig >= NSIG) { + errno = EINVAL; + return SIG_ERR; + } + old = currentHandler[sig]; + if (func == SIG_DFL || func == SIG_IGN) { + userHandlers[sig] = 0; + Ksignal(sig, (void *)func, &err); + } else { + __gnoCaptureDP(); + userHandlers[sig] = (void (*)(int))func; + Ksignal(sig, (void *)&__sigTrampoline, &err); + } + currentHandler[sig] = func; + return old; +} + + +// sigprocmask: examine/change the blocked-signal mask, over Ksigblock (OR-in) +// and Ksigsetmask (replace). Ksigblock(0) reads the current mask race-free. +int sigprocmask(int how, const sigset_t *set, sigset_t *oset) { + int err = 0; + unsigned long old; + unsigned long newSet; + newSet = set ? (unsigned long)*set : 0; + switch (how) { + case SIG_BLOCK: + old = Ksigblock((void *)newSet, &err); + break; + case SIG_UNBLOCK: + old = Ksigblock((void *)0, &err); // read current + Ksigsetmask((void *)(old & ~newSet), &err); // clear the bits + break; + case SIG_SETMASK: + old = Ksigsetmask((void *)newSet, &err); + break; + default: + errno = EINVAL; + return -1; + } + if (oset) { + *oset = old; + } + return 0; +} + + +// sigsetmask: BSD -- replace the blocked set, return the old mask. +unsigned long sigsetmask(unsigned long mask) { + int err = 0; + return Ksigsetmask((void *)mask, &err); +} + + +// sigsuspend: atomically install mask and block until a signal is delivered. +// Ksigpause swaps the mask in, sleeps, and restores it on wake; POSIX +// sigsuspend always reports EINTR. +int sigsuspend(const sigset_t *mask) { + int err = 0; + Ksigpause((void *)(mask ? (unsigned long)*mask : 0), &err); + errno = EINTR; + return -1; +} diff --git a/runtime/src/libgcc.s b/runtime/src/libgcc.s index 1e07269..47e01fe 100644 --- a/runtime/src/libgcc.s +++ b/runtime/src/libgcc.s @@ -43,17 +43,19 @@ ; C ABI) survive the indirection unmodified. ; ; Why the SMC dispatch instead of a fixed `jmp ($00B8)`: -; Under bare-metal (crt0.s) and GNO/ME (crt0Gno.s) the runtime sets -; 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) we now keep DP at the Loader-allocated value rather -; than forcing DP=0 — that keeps every codegen scratch slot (the +; 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 Loader-reserved bank-0 memory instead of -; on top of system zero page that ROM IRQ handlers and GS/OS -; dispatcher state freely scribble. Forcing DP=0 was making clang- -; built S16 apps with C++ ctors crash during the init_array -; indirect-dispatch loop for exactly that reason. +; 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. ; ; 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 @@ -87,8 +89,9 @@ __jsl_indir: .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); default - ; $00B8 is correct when DP=0 (bare-metal, GNO). + ; 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. ; -------------------------------------------------------------------- ; __run_cxa_atexit — weak no-op fallback. diff --git a/scripts/genGnoKernel.py b/scripts/genGnoKernel.py index 11ace72..92ef079 100644 --- a/scripts/genGnoKernel.py +++ b/scripts/genGnoKernel.py @@ -3,8 +3,9 @@ # # The GNO kernel is implemented as Apple IIgs user toolset $03. Each # kernel function is identified by a 16-bit number $XX03 where XX is -# the function index and 03 is the toolset. The dispatcher is the -# standard IIgs tool dispatcher at $E10000. +# the function index and 03 is the toolset. Kernel calls dispatch through +# GNO's own vector $E10008 (the "udispatch" JSL target GNO installs), NOT the +# standard IIgs tool dispatcher $E10000 (toolset $03 there is the Misc Tool Set). # # Function list is hard-coded from include/gno/kerntool.h in the GNO # Consortium source (https://github.com/GnoConsortium/gno). The @@ -13,7 +14,7 @@ # Output: runtime/src/gnoKernel.s — asm wrappers callable from our C # ABI (arg0 in A, arg0 i32 in A:X, rest pushed RTL on stack). Each # wrapper re-pushes args in toolbox/Pascal order (high-word first for -# 32-bit values), allocates result space, JSLs $E10000, pops the +# 32-bit values), allocates result space, JSLs $E10008, pops the # result back into A:X if non-void. # # Run after editing the function table: @@ -84,7 +85,11 @@ SYSCALLS = [ (0x4103, "KSetGNOQuitRec", 0, [2, 4, 2, 4]), # (pCount, GSStringPtr, flags, *err) -> void ] -DISPATCHER = 0xE10000 +# GNO kernel calls dispatch through GNO's OWN vector $E10008 (the "udispatch" +# JSL target GNO installs -- lib/libc/sys/trap.asm: `udispatch gequ $E10008`), +# NOT the standard IIgs Tool Locator at $E10000 (where toolset $03 is the Misc +# Tool Set, so calling it there hangs/misbehaves). Same LDX #funcId convention. +DISPATCHER = 0xE10008 def emitWrapper(funcId, name, retSize, argSizes): @@ -205,7 +210,7 @@ def main(): "; AUTOGENERATED by scripts/genGnoKernel.py — DO NOT EDIT by hand.", ";", "; GNO/ME kernel toolset $03 wrappers.", - "; Dispatcher: JSL $E10000 with LDX #funcId.", + "; Dispatcher: JSL $E10008 (GNO's udispatch vector) with LDX #funcId.", ";", "; C ABI: arg0 (i16) in A, arg0 (i32) in A:X, arg1+ on stack", "; (caller-pushed, lo-word first for 32-bit values).", diff --git a/scripts/runInGno.sh b/scripts/runInGno.sh index 4f3d3f5..fcb3f40 100755 --- a/scripts/runInGno.sh +++ b/scripts/runInGno.sh @@ -72,6 +72,13 @@ cp "$PROG" "$WORK/${NAME}#B50100" cp "$PROG" "$WORK/HELLO#B50100" "$CADIUS" ADDFILE "$DATA" /GNO.BOOT/bin "$WORK/${NAME}#B50100" >/dev/null "$CADIUS" ADDFILE "$DATA" /GNO.BOOT "$WORK/HELLO#B50100" >/dev/null +# Optional second executable for concurrency tests: added to /bin so gsh +# can run e.g. GNO_RUNCMD="proga & progb". Backward-compatible (unset -> no-op). +if [ -n "${GNO_PROG2:-}" ] && [ -f "${GNO_PROG2}" ]; then + NAME2=$(basename "$GNO_PROG2" | sed 's/\.[^.]*$//' | tr '[:upper:]' '[:lower:]' | cut -c1-15) + cp "$GNO_PROG2" "$WORK/${NAME2}#B50100" + "$CADIUS" ADDFILE "$DATA" /GNO.BOOT/bin "$WORK/${NAME2}#B50100" >/dev/null +fi # Optional: drop an extra TXT file in /home/root (the login cwd) for # stdin-redirect tests. Reference it relatively or as /home/root/. if [ -n "${GNO_ADDFILE:-}" ] && [ -f "${GNO_ADDFILE}" ]; then diff --git a/scripts/smokeTest.sh b/scripts/smokeTest.sh index e64cfb9..8fe7056 100755 --- a/scripts/smokeTest.sh +++ b/scripts/smokeTest.sh @@ -6977,6 +6977,177 @@ else } log "OK: cxxChronoProbe steady_clock monotonic + i32 rep verified under GNO" + # Phase 5.3b heap: gnoHeapProbe -- regression for the bank-0 malloc-heap + # collision. crt0Gno now backs malloc with an MM-owned handle + # (__heapInitMM); before that, malloc bumped the fixed bank-0 link-time + # window [__bss_end,0xBF00) into GNO's kernel heartbeat block -> SysFailMgr + # $0308. This mallocs 8 KB (past the old collision point) and verifies + # every block survives intact. + log "check: gnoHeapProbe (MM-backed malloc heap, 8 KB) runs under GNO" + bash "$PROJECT_ROOT/demos/buildGno.sh" gnoHeapProbe >/tmp/gnoHeapBuildOut 2>&1 || { + cat /tmp/gnoHeapBuildOut >&2 + die "buildGno.sh gnoHeapProbe failed" + } + bash "$PROJECT_ROOT/scripts/runInGno.sh" "$PROJECT_ROOT/demos/gnoHeapProbe.omf" \ + --check 0x025010=BEEF --check 0x025012=0001 --check 0x025000=C0DE \ + >/tmp/gnoHeapRunOut 2>&1 || { + cat /tmp/gnoHeapRunOut >&2 + die "gnoHeapProbe failed: malloc-heap bank-0/heartbeat collision or marker 0xC0DE not reached" + } + log "OK: gnoHeapProbe MM-backed heap survived 8 KB of allocations under GNO" + + # Phase 5.3c large-BSS: gnoBigBssProbe -- regression for the crt0Gno + # BSS-clear heartbeat clobber. A ~6 KB zero-init BSS array pushes + # __bss_end past the old ~0xB100 corruption threshold; crt0Gno no longer + # clears BSS (the loader fills the embedded zeros), so the program must + # run AND read the array back as all-zero. + log "check: gnoBigBssProbe (large zero-init BSS) runs under GNO" + bash "$PROJECT_ROOT/demos/buildGno.sh" gnoBigBssProbe >/tmp/gnoBigBssBuildOut 2>&1 || { + cat /tmp/gnoBigBssBuildOut >&2 + die "buildGno.sh gnoBigBssProbe failed" + } + bash "$PROJECT_ROOT/scripts/runInGno.sh" "$PROJECT_ROOT/demos/gnoBigBssProbe.omf" \ + --check 0x025010=BEEF --check 0x025012=0001 --check 0x025000=C0DE \ + >/tmp/gnoBigBssRunOut 2>&1 || { + cat /tmp/gnoBigBssRunOut >&2 + die "gnoBigBssProbe failed: large-BSS heartbeat clobber regressed or BSS not zero-filled" + } + log "OK: gnoBigBssProbe large BSS zero-filled + no heartbeat clobber under GNO" + + # Phase 5.3d process lifecycle: gnoForkWaitProbe -- the FIRST exercise of the + # GNO kernel syscall surface (K* toolset $03). Regression for two fixed bugs: + # (1) gnoKernel.s dispatched every K* call via $E10000 (the GS/OS Tool Locator, + # where toolset $03 is Misc Tools) instead of GNO's udispatch vector $E10008, + # so any kernel call hung -- latent because nothing exercised a K* primitive + # until now. (2) _exit recorded 0x2029 as the exit code (__gnoGsosCall left + # A = the QUIT callNum at the trap, which StackGSOS stashes as exitCode), so + # wait() decoded WEXITSTATUS = 41; __gnoQuit now leaves A = status. This + # fork()s a leaf child that _exit(42)s and asserts the parent's wait() reaps + # the same pid with WEXITSTATUS == 42 (0x2A), guarding the old 41 (0x29). + log "check: gnoForkWaitProbe (fork thread-spawn + wait + _exit status) under GNO" + bash "$PROJECT_ROOT/demos/buildGno.sh" gnoForkWaitProbe >/tmp/gnoForkWaitBuildOut 2>&1 || { + cat /tmp/gnoForkWaitBuildOut >&2 + die "buildGno.sh gnoForkWaitProbe failed" + } + bash "$PROJECT_ROOT/scripts/runInGno.sh" "$PROJECT_ROOT/demos/gnoForkWaitProbe.omf" \ + --check 0x025000=C0DE --check 0x025010=C41D --check 0x025014=002A \ + --check 0x025016=0001 --check 0x025018=600D \ + >/tmp/gnoForkWaitRunOut 2>&1 || { + cat /tmp/gnoForkWaitRunOut >&2 + die "gnoForkWaitProbe failed: K* dispatch vector, fork thread-spawn, or _exit status regressed" + } + log "OK: gnoForkWaitProbe fork/wait + _exit status (WEXITSTATUS=42) green under GNO" + + # 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 + # exercises one subsystem end-to-end under real GNO/MAME. (These all depend + # on the $E10008 kernel-dispatch fix -- see Phase 5.3d.) + for gnoProbeSpec in \ + "gnoWaitpidTimesProbe|waitpid + times|0x025000=C0DE 0x025010=C41D 0x025014=0025 0x025016=0001 0x025018=0001" \ + "gnoStatProbe|stat/fstat/isatty|0x025000=C0DE 0x025010=0001 0x025012=0001 0x025014=0001 0x025016=0001 0x025018=0001" \ + "gnoIdProbe|uid/gid + job control|0x025000=C0DE 0x025010=0001 0x025012=0001 0x025014=0001 0x025016=0001" \ + "gnoEnvProbe|environment setenv/getenv|0x025000=C0DE 0x025012=0001 0x025002=0001" \ + "gnoFdProbe|POSIX fd layer|0x025000=C0DE 0x025002=0001 0x025004=0001 0x025006=0001 0x025008=0001 0x02500A=0001 0x02500C=0001 0x02500E=0001" \ + "gnoSignalProbe|signal + __sigTrampoline SIGALRM|0x025000=C0DE 0x025010=C41D 0x025012=0001" \ + ; do + gnoProbe="${gnoProbeSpec%%|*}" + gnoRest="${gnoProbeSpec#*|}" + gnoDesc="${gnoRest%%|*}" + gnoChecks="${gnoRest##*|}" + gnoCheckArgs="" + for gnoChk in $gnoChecks; do + gnoCheckArgs="$gnoCheckArgs --check $gnoChk" + done + log "check: $gnoProbe ($gnoDesc) under GNO" + bash "$PROJECT_ROOT/demos/buildGno.sh" "$gnoProbe" >/tmp/gnoSyscallBuildOut 2>&1 || { + cat /tmp/gnoSyscallBuildOut >&2 + die "buildGno.sh $gnoProbe failed" + } + bash "$PROJECT_ROOT/scripts/runInGno.sh" "$PROJECT_ROOT/demos/$gnoProbe.omf" $gnoCheckArgs \ + >/tmp/gnoSyscallRunOut 2>&1 || { + cat /tmp/gnoSyscallRunOut >&2 + die "$gnoProbe failed: GNO POSIX syscall wrapper regression ($gnoDesc)" + } + log "OK: $gnoProbe ($gnoDesc) green under GNO" + done + + # Phase 5.3l exec-INTO: gnoExecProbe run WITH a second program deployed on + # /bin (GNO_PROG2), so its final execvp("gnoexeced") actually replaces the + # process image via GS/OS InitialLoad2. This is the exec keystone the + # generic loop above cannot cover (it needs a companion binary on the disk). + # + # Half-1 markers (02/04/08/0A) come from prog1: access() over Kstat + a + # failed $PATH-resolved execvp. The exec-INTO markers are the payoff: + # 0x025010 = E0EC gnoExeced ran (K_execve transferred control) + # 0x025000 = C0DE reached end, set BY the exec'd 2nd program + # Regression guard for the DP/Stack size ceiling: a program's ~Direct $12 + # segment must be small enough that the caller's AND callee's copies fit in + # bank 0 at once during InitialLoad2 (see demos/buildGno.sh --stack-size + # note). A 16 KB stack regresses this to EIO / no transfer. + log "check: gnoExecProbe exec-INTO (execvp replaces image with gnoExeced) under GNO" + bash "$PROJECT_ROOT/demos/buildGno.sh" gnoExeced >/tmp/gnoExecedBuildOut 2>&1 || { + cat /tmp/gnoExecedBuildOut >&2 + die "buildGno.sh gnoExeced failed" + } + bash "$PROJECT_ROOT/demos/buildGno.sh" gnoExecProbe >/tmp/gnoExecBuildOut 2>&1 || { + cat /tmp/gnoExecBuildOut >&2 + die "buildGno.sh gnoExecProbe failed" + } + GNO_PROG2="$PROJECT_ROOT/demos/gnoExeced.omf" \ + bash "$PROJECT_ROOT/scripts/runInGno.sh" "$PROJECT_ROOT/demos/gnoExecProbe.omf" \ + --check 0x025000=C0DE --check 0x025002=0001 --check 0x025004=0001 \ + --check 0x025008=0001 --check 0x02500A=0001 --check 0x025010=E0EC \ + >/tmp/gnoExecRunOut 2>&1 || { + cat /tmp/gnoExecRunOut >&2 + die "gnoExecProbe exec-into failed: K_execve did not transfer control (DP/Stack size ceiling? see buildGno.sh)" + } + log "OK: gnoExecProbe exec-INTO transferred control to gnoExeced under GNO" + + # Phase 5.3m errno-space consistency: GNO numbers its kernel errno differently + # from our (GNO ENOENT=4 vs our 2). The fd layer maps GS/OS errors + # via _mapErr; the stat/access/process wrappers translate the RAW kernel errno + # via gnoToPosixErrno (libcGno.c). This probe triggers a missing-file error + # through open/stat/unlink/access and asserts all four report POSIX ENOENT + # (0x02503C=0001) -- guards against the raw GNO errno leaking back through. + log "check: gnoErrnoProbe (errno-space: stat/access map GNO errno -> POSIX) under GNO" + bash "$PROJECT_ROOT/demos/buildGno.sh" gnoErrnoProbe >/tmp/gnoErrnoBuildOut 2>&1 || { + cat /tmp/gnoErrnoBuildOut >&2 + die "buildGno.sh gnoErrnoProbe failed" + } + bash "$PROJECT_ROOT/scripts/runInGno.sh" "$PROJECT_ROOT/demos/gnoErrnoProbe.omf" \ + --check 0x025030=0002 --check 0x025032=0002 --check 0x025034=0002 \ + --check 0x025036=0002 --check 0x025038=0002 --check 0x02503A=0001 \ + --check 0x02503C=0001 --check 0x025000=C0DE \ + >/tmp/gnoErrnoRunOut 2>&1 || { + cat /tmp/gnoErrnoRunOut >&2 + die "gnoErrnoProbe failed: stat/access errno not mapped to POSIX ENOENT (gnoToPosixErrno regression)" + } + log "OK: gnoErrnoProbe (stat/access errno == POSIX ENOENT) under GNO" + + # Phase 5.3n composite: ONE process that chains env + file I/O + stat + fork/ + # waitpid + signals + times + exec-into, in an order that stresses the cross- + # subsystem interactions the isolated probes miss (fork AFTER file I/O, signals + # AFTER fork, exec AFTER all of it). The end-to-end "real program" smoke: if a + # future change breaks an INTERACTION the per-subsystem probes still pass but + # this one catches it. (gnoExeced.omf was built by the exec-INTO stage above.) + log "check: gnoComposite (env+file+stat+fork+signal+times+exec in one process) under GNO" + bash "$PROJECT_ROOT/demos/buildGno.sh" gnoComposite >/tmp/gnoCompositeBuildOut 2>&1 || { + cat /tmp/gnoCompositeBuildOut >&2 + die "buildGno.sh gnoComposite failed" + } + GNO_PROG2="$PROJECT_ROOT/demos/gnoExeced.omf" \ + bash "$PROJECT_ROOT/scripts/runInGno.sh" "$PROJECT_ROOT/demos/gnoComposite.omf" \ + --check 0x025002=0007 --check 0x025020=0001 --check 0x025022=0001 \ + --check 0x025024=0001 --check 0x025026=0001 --check 0x025028=0001 \ + --check 0x02502A=0001 --check 0x02502E=C41D --check 0x025010=E0EC \ + --check 0x025000=C0DE \ + >/tmp/gnoCompositeRunOut 2>&1 || { + cat /tmp/gnoCompositeRunOut >&2 + die "gnoComposite failed: cross-subsystem composition regression under GNO" + } + log "OK: gnoComposite (7 subsystems composed in one process) green under GNO" + # Phase 5.4 cxxstream+format+path: build cxxStreamProbe and run under # GNO/MAME. Verifies: # - etl::string_stream< emitDpStackSeg(uint32_t length, uint16_t segNum) { // bss-as-zeros approach used for the user CODE seg — the Loader's // ExpressLoad fast path can't be trusted to honor RESSPC). const uint32_t RESSPC = 0; - const uint32_t BANKSIZE = 0; // DP/Stack lives in bank 0 + const uint32_t BANKSIZE = 0x10000; // must fit within one bank (bank 0). BANKSIZE=0 means "may cross banks", which the GS/OS Loader's InitialLoad2 path (used by GNO execve) rejects for a DP/Stack seg; a real GNO ~_STACK uses 0x10000. const uint32_t ALIGN = 0x100; // page-aligned per spec const uint16_t KIND = OMF_KIND_DPSTACK; // DP/Stack | RELOAD diff --git a/src/llvm/lib/Target/W65816/W65816UnLSR.cpp b/src/llvm/lib/Target/W65816/W65816UnLSR.cpp index 5143cb1..6fbee40 100644 --- a/src/llvm/lib/Target/W65816/W65816UnLSR.cpp +++ b/src/llvm/lib/Target/W65816/W65816UnLSR.cpp @@ -45,6 +45,7 @@ #include "llvm/ADT/SmallVector.h" #include "llvm/Analysis/LoopInfo.h" #include "llvm/Analysis/ScalarEvolution.h" +#include "llvm/IR/Dominators.h" #include "llvm/InitializePasses.h" #include "llvm/IR/Constants.h" #include "llvm/IR/Function.h" @@ -72,6 +73,7 @@ public: void getAnalysisUsage(AnalysisUsage &AU) const override { AU.addRequired(); + AU.addRequired(); AU.setPreservesCFG(); } @@ -79,7 +81,7 @@ public: private: bool processLoop(Loop *L); - bool processCounterToPtrPHIs(Loop *L); + bool processCounterToPtrPHIs(Loop *L, DominatorTree &DT); }; } // namespace @@ -89,6 +91,7 @@ char W65816UnLSR::ID = 0; INITIALIZE_PASS_BEGIN(W65816UnLSR, DEBUG_TYPE, "W65816 undo LSR for global-array", false, false) INITIALIZE_PASS_DEPENDENCY(LoopInfoWrapperPass) +INITIALIZE_PASS_DEPENDENCY(DominatorTreeWrapperPass) INITIALIZE_PASS_END(W65816UnLSR, DEBUG_TYPE, "W65816 undo LSR for global-array", false, false) @@ -98,15 +101,16 @@ bool W65816UnLSR::runOnFunction(Function &F) { if (F.hasOptNone()) return false; LoopInfo &LI = getAnalysis().getLoopInfo(); + DominatorTree &DT = getAnalysis().getDomTree(); bool Changed = false; for (Loop *L : LI) { Changed |= processLoop(L); - Changed |= processCounterToPtrPHIs(L); + Changed |= processCounterToPtrPHIs(L, DT); SmallVector Worklist(L->begin(), L->end()); while (!Worklist.empty()) { Loop *Sub = Worklist.pop_back_val(); Changed |= processLoop(Sub); - Changed |= processCounterToPtrPHIs(Sub); + Changed |= processCounterToPtrPHIs(Sub, DT); Worklist.append(Sub->begin(), Sub->end()); } } @@ -131,7 +135,7 @@ bool W65816UnLSR::runOnFunction(Function &F) { // Rewrite: for each base_i, introduce a pointer PHI that strides by 1 // per iter. Replace %scevgep_i with the new pointer PHI. If counter // has no other uses, eliminate it. -bool W65816UnLSR::processCounterToPtrPHIs(Loop *L) { +bool W65816UnLSR::processCounterToPtrPHIs(Loop *L, DominatorTree &DT) { BasicBlock *Header = L->getHeader(); BasicBlock *Latch = L->getLoopLatch(); BasicBlock *Preheader = L->getLoopPreheader(); @@ -179,8 +183,17 @@ bool W65816UnLSR::processCounterToPtrPHIs(Loop *L) { Value *Base = GEP->getPointerOperand(); // base must be loop-invariant. Instructions inside the loop fail; // arguments and globals are always invariant. - if (auto *BaseI = dyn_cast(Base)) + if (auto *BaseI = dyn_cast(Base)) { if (L->contains(BaseI)) return false; + // Base becomes the pointer-PHI's preheader incoming value, so it must + // dominate the preheader's terminator (the edge into the header). A + // value merely OUTSIDE the loop is not guaranteed to: a compound loop + // guard (a && b) can define an invariant GEP on a sibling path that + // reaches the preheader without dominating it. Emitting a PHI with a + // non-dominating incoming there fails the verifier ("Instruction does + // not dominate all uses"), so skip the transform for this loop instead. + if (!DT.dominates(BaseI, Preheader->getTerminator())) return false; + } if (!Base->getType()->isPointerTy()) return false; // Only handle the i8 element type (byte stride). Other strides // would need different ptr-PHI step values.