From 8c5c5ac1d24fc4fbca5fbab4e3e534b2746031f1 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sun, 5 Jul 2026 23:30:54 -0500 Subject: [PATCH] JoeyLib fixes. --- SESSION_STATE.md | 8 ++++++ runtime/src/crt0Gsos.s | 15 +++++++++++ runtime/src/libc.c | 56 +++++++++++++++++++++++++++++++++++++++++ runtime/src/libgcc.s | 14 +++++++++++ scripts/smokeTest.sh | 6 ++++- src/link816/link816.cpp | 16 ++++++++++++ src/link816/omfEmit.cpp | 19 ++++++++++++++ 7 files changed, 133 insertions(+), 1 deletion(-) diff --git a/SESSION_STATE.md b/SESSION_STATE.md index aedc895..9afb705 100644 --- a/SESSION_STATE.md +++ b/SESSION_STATE.md @@ -4,6 +4,14 @@ Drop this into a new Claude Code session and say "read SESSION_STATE.md and continue where we left off." Pairs with `LLVM_65816_DESIGN.md` (the design doc — read that second). +> **INCOMING ASK (2026-07-05, from the JoeyLib session):** see +> `JOEYLIB-ASKS.md` in this directory — loader/crt0 must MM-reserve +> segment BSS spans (their item 5, CRITICAL). New evidence: an app-side +> claim recipe that WORKS for reading the spans (DATA32 table over the +> `__bss_seg*` symbols) but a successful NewHandle claim over the app's +> own BSS kills the app — so the reservation has to happen loader-side. +> Blocks JoeyLib's IIgs sprite codegen and the Space Taxi port. + --- ## 1. Project in one sentence diff --git a/runtime/src/crt0Gsos.s b/runtime/src/crt0Gsos.s index 62c4425..0424fec 100644 --- a/runtime/src/crt0Gsos.s +++ b/runtime/src/crt0Gsos.s @@ -92,6 +92,21 @@ __start: .Lbss_done: rep #0x20 ; restore M=16 + ; Back the C malloc heap with a locked Memory-Manager handle so the + ; heap span is MM-owned -- otherwise a later NewHandle (ours or the + ; 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. + rep #0x30 + pha ; MMStartUp result space (word) + ldx #0x0202 ; MMStartUp call number + jsl 0xe10000 ; Tool Locator dispatch -> A = our MM user ID + pla + jsl __heapInitMM ; __heapInitMM(userID); arg0 in A + ; Walk .init_array (C++ ctors). The `jsl __jsl_indir` and the ; `jsl main` below are relocated by the GS/OS Loader: omfEmit emits ; a cRELOC (0xF5) IMM24 site for each intra-segment JSL, so the diff --git a/runtime/src/libc.c b/runtime/src/libc.c index 1451cdc..a795712 100644 --- a/runtime/src/libc.c +++ b/runtime/src/libc.c @@ -537,6 +537,62 @@ static void mallocInitOnce(void) { freeList = (FreeBlk *)0; } +// 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. +// +// On any failure -- NewHandle returns 0, a null master pointer, or a block +// that straddles a bank boundary -- it returns without setting bumpPtr, so +// mallocInitOnce falls back to the link-time [__heap_start,__heap_end) +// window. A tight-memory machine is therefore never worse off than before. +extern void *NewHandle(unsigned long size, unsigned short userID, + unsigned short attr, void *loc); +extern void DisposeHandle(void *h); +void __heapInitMM(unsigned short userID) { + if (bumpPtr) { + return; // already initialised + } + // Heap size: keep the link-time window when it is in a sane range, + // else a 32 KB default. The window is bank-0-bounded, so it never + // spans more than a single bank. + unsigned long want = (unsigned long)(void *)__heap_end + - (unsigned long)(void *)__heap_start; + if (want < 0x2000UL || want > 0xC000UL) { + want = 0x8000UL; // 32 KB + } + // attr 0x8014 = attrLocked | attrNoCross | attrPage. Every bit here is + // set in the proven-good desktop.c DP-block value 0xC015, minus + // attrFixed and attrBank -- so the MM is free to place the block in any + // bank and then pin it. attrLocked keeps the master pointer stable, + // which malloc relies on (it hands out raw pointers into the block). + void *h = NewHandle(want, userID, (unsigned short)0x8014, (void *)0); + if (!h) { + return; // fall back to link-time window + } + char *base = *(char **)h; + if (!base) { + DisposeHandle(h); + return; + } + // The allocator does flat pointer arithmetic and assumes a single bank; + // reject (rather than corrupt on) a straddling block. This also makes + // correctness independent of the attrNoCross bit guess above. + unsigned long lo = (unsigned long)(void *)base; + unsigned long hi = lo + want - 1UL; + if ((lo & 0xFF0000UL) != (hi & 0xFF0000UL)) { + DisposeHandle(h); + return; + } + bumpPtr = base; + heapEnd = base + want; + freeList = (FreeBlk *)0; +} + void *malloc(size_t n0) { mallocInitOnce(); // Heap ceiling is ~32KB so anything > 0x7FF0 is unsatisfiable. diff --git a/runtime/src/libgcc.s b/runtime/src/libgcc.s index 744b838..1e07269 100644 --- a/runtime/src/libgcc.s +++ b/runtime/src/libgcc.s @@ -114,6 +114,20 @@ __run_cxa_atexit: __srandInitFromTime: rtl +; -------------------------------------------------------------------- +; __heapInitMM — weak no-op fallback. +; +; crt0Gsos calls `jsl __heapInitMM` (Memory-Manager user ID in A) before +; .init_array to back the malloc heap with a locked MM handle. GS/OS +; programs that don't link libc.o (bare probes / link tests) must still +; resolve the symbol; the no-op leaves malloc on its link-time +; [__heap_start,__heap_end) window. libc.o overrides with the strong +; version. A is ignored. +; -------------------------------------------------------------------- + .weak __heapInitMM +__heapInitMM: + rtl + ; -------------------------------------------------------------------- ; __mulhi3 — 16-bit multiply. A * (4,S) -> A. ; Signed and unsigned share an implementation: only the low 16 bits of diff --git a/scripts/smokeTest.sh b/scripts/smokeTest.sh index 15ee7ca..a577590 100755 --- a/scripts/smokeTest.sh +++ b/scripts/smokeTest.sh @@ -5643,7 +5643,11 @@ while pos < len(data): kind = struct.unpack_from('second < seg.base + seg.body.size()) { entryOff = it->second - seg.base; } + // BSS attribution: link() places ALL of rodata/bss/init_array + // in segment 1's bank (see the layout comment near link() — + // "Other sections ... stay in segment 1's bank"). So the + // whole merged BSS span belongs to segment 1 (segments[0]); + // every other segment carries no BSS. Emit bss_start (the + // absolute link address of BSS) and bss_size so omfEmit can + // embed the span as LCONST zeros — giving the GS/OS Loader's + // per-segment NewHandle coverage of BSS. Without this the BSS + // is phantom RAM the Memory Manager can hand to a later + // NewHandle (segment/heap overlap corruption), and it is never + // zeroed under GS/OS (crt0Gsos relies on the Loader LCONST-fill). + uint32_t segBssStart = (k == 0) ? lastLayout.bssBase : 0u; + uint32_t segBssSize = (k == 0) ? lastLayout.bssSize : 0u; std::snprintf(buf, sizeof(buf), " {\n" " \"num\": %u,\n" " \"name\": \"SEG%u\",\n" " \"base\": \"0x%06x\",\n" " \"size\": %zu,\n" + " \"bss_start\": \"0x%06x\",\n" + " \"bss_size\": %u,\n" " \"image\": \"%s\",\n" " \"entry_offset\": \"0x%04x\"\n" " }%s\n", seg.segNum, seg.segNum, seg.base, seg.body.size(), + segBssStart, segBssSize, imgPath.c_str(), entryOff, (k + 1 < lastLayout.segments.size()) ? "," : ""); mf << buf; diff --git a/src/link816/omfEmit.cpp b/src/link816/omfEmit.cpp index 4e7376e..66f5404 100644 --- a/src/link816/omfEmit.cpp +++ b/src/link816/omfEmit.cpp @@ -815,6 +815,8 @@ struct ManifestSeg { uint32_t num = 0; uint32_t base = 0; uint32_t entryOff = 0; + uint32_t bssStart = 0; // absolute link addr of this seg's BSS (0 = none) + uint32_t bssSize = 0; // BSS span size in bytes (embedded as LCONST zeros) std::string image; std::string name; }; @@ -887,6 +889,8 @@ static std::vector parseManifest(const std::string &path) { seg.num = extractNumberField(block, "num"); seg.base = extractNumberField(block, "base"); seg.entryOff = extractNumberField(block, "entry_offset"); + seg.bssStart = extractNumberField(block, "bss_start"); // 0 if absent + seg.bssSize = extractNumberField(block, "bss_size"); // 0 if absent seg.image = extractStringField(block, "image"); seg.name = extractStringField(block, "name"); if (seg.image.empty()) die("manifest segment missing 'image'"); @@ -1048,6 +1052,21 @@ int main(int argc, char **argv) { u.image = readFile(s.image); u.entryOffset = (k == 0) ? s.entryOff : 0; u.name = s.name; + // Embed this segment's BSS as trailing LCONST zeros so the + // GS/OS Loader's per-segment NewHandle covers the BSS span + // (otherwise it is phantom RAM the Memory Manager can hand + // to a later NewHandle -> corruption; it is also never + // zeroed since crt0Gsos relies on the Loader's LCONST fill). + // Same computation as the single-seg path: bss_gap pads + // from image-end up to the link-layout BSS address. + if (s.bssSize > 0) { + u.bssSize = s.bssSize; + if (s.bssStart >= s.base) { + uint32_t bssOff = s.bssStart - s.base; + if (bssOff > u.image.size()) + u.bssGap = bssOff - (uint32_t)u.image.size(); + } + } if (!relocFile.empty()) { char perSegPath[512]; std::snprintf(perSegPath, sizeof(perSegPath),