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