JoeyLib fixes.

This commit is contained in:
Scott Duensing 2026-07-05 23:30:54 -05:00
parent fb2f032923
commit 8c5c5ac1d2
7 changed files with 133 additions and 1 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -5643,7 +5643,11 @@ while pos < len(data):
kind = struct.unpack_from('<H', data, pos+20)[0]
ok_kinds.append(kind)
pos += bytecnt
expected = [0x8001, 0x1000, 0x1000]
# Root user seg (seg 2 of the OMF) carries the DYNAMIC bit (0x1100 =
# CODE|PRIV|DYNAMIC); non-root user segs stay 0x1000. Real GS/OS
# rejects a multi-seg file whose root lacks DYNAMIC (see omfEmit
# segKind logic) -- an earlier stale expectation here was [.,0x1000,.].
expected = [0x8001, 0x1100, 0x1000]
print('OK' if ok_kinds == expected else 'BAD ' + str(ok_kinds))
")
if [ "$nPhCSeg" != "OK" ]; then

View file

@ -2175,16 +2175,32 @@ struct Linker {
it->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;

View file

@ -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<ManifestSeg> 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),