Another pass at GNO.

This commit is contained in:
Scott Duensing 2026-07-07 18:33:48 -05:00
parent 8d0bac168e
commit 8d30c06605
36 changed files with 3198 additions and 159 deletions

View file

@ -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

29
demos/gnoBigBssProbe.c Normal file
View file

@ -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 <stdint.h>
// ~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;
}

199
demos/gnoComposite.c Normal file
View file

@ -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 <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/times.h>
#include <signal.h>
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;
}

42
demos/gnoEnvProbe.c Normal file
View file

@ -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 <stdint.h>
#include <stdlib.h>
#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;
}

93
demos/gnoErrnoProbe.c Normal file
View file

@ -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 <stdint.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#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;
}

65
demos/gnoExecDiag.c Normal file
View file

@ -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 <stdint.h>
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;
}

81
demos/gnoExecProbe.c Normal file
View file

@ -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 <stdint.h>
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;
}

21
demos/gnoExeced.c Normal file
View file

@ -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 <stdint.h>
#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;
}

103
demos/gnoFdProbe.c Normal file
View file

@ -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 <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#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;
}

56
demos/gnoForkWaitProbe.c Normal file
View file

@ -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 <stdint.h>
#include <sys/wait.h>
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;
}

41
demos/gnoHeapProbe.c Normal file
View file

@ -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 <stdint.h>
#include <stdlib.h>
#include <string.h>
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;
}

47
demos/gnoIdProbe.c Normal file
View file

@ -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 <stdint.h>
#include <unistd.h>
#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;
}

43
demos/gnoSignalProbe.c Normal file
View file

@ -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 <stdint.h>
#include <signal.h>
#include <unistd.h>
#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;
}

72
demos/gnoStatProbe.c Normal file
View file

@ -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 <stdint.h>
#include <sys/stat.h>
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;
}

View file

@ -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 <stdint.h>
#include <sys/wait.h>
#include <sys/times.h>
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;
}

Binary file not shown.

49
runtime/include/fcntl.h Normal file
View file

@ -0,0 +1,49 @@
// fcntl.h -- file-control definitions for the GNO/ME runtime.
//
// O_* values match GNO's <sys/fcntl.h> 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 <sys/types.h> // mode_t / off_t (single source of truth)
#ifdef __cplusplus
extern "C" {
#endif
// Access modes (open-only), matching GNO <sys/fcntl.h>.
#define O_RDONLY 0x0001
#define O_WRONLY 0x0002
#define O_RDWR 0x0004
#define O_ACCMODE 0x0007
// Status / creation flags, matching GNO <sys/fcntl.h>.
#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 <stdio.h>'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

View file

@ -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

View file

@ -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

100
runtime/include/sys/stat.h Normal file
View file

@ -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 <sys/types.h>
#include <time.h> // 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

View file

@ -0,0 +1,25 @@
// sys/times.h -- process CPU-time accounting for the GNO/ME runtime.
//
// Layout byte-matches GNO's own <sys/times.h> (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 <time.h>
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

View file

@ -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 <time.h> (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

View file

@ -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

87
runtime/include/unistd.h Normal file
View file

@ -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 <fcntl.h> / <sys/stat.h>.
#ifndef _UNISTD_H
#define _UNISTD_H
#include <stddef.h>
#include <sys/types.h> // 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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];

File diff suppressed because it is too large Load diff

View file

@ -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.

View file

@ -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).",

View file

@ -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/<name>.
if [ -n "${GNO_ADDFILE:-}" ] && [ -f "${GNO_ADDFILE}" ]; then

View file

@ -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 <errno.h> (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<<int produces the expected "x=42" — the

View file

@ -369,7 +369,7 @@ static std::vector<uint8_t> 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

View file

@ -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<LoopInfoWrapperPass>();
AU.addRequired<DominatorTreeWrapperPass>();
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<LoopInfoWrapperPass>().getLoopInfo();
DominatorTree &DT = getAnalysis<DominatorTreeWrapperPass>().getDomTree();
bool Changed = false;
for (Loop *L : LI) {
Changed |= processLoop(L);
Changed |= processCounterToPtrPHIs(L);
Changed |= processCounterToPtrPHIs(L, DT);
SmallVector<Loop *, 4> 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<Instruction>(Base))
if (auto *BaseI = dyn_cast<Instruction>(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.