diff --git a/runtime/src/libc.c b/runtime/src/libc.c index 68b1f1d..34af6f6 100644 --- a/runtime/src/libc.c +++ b/runtime/src/libc.c @@ -574,7 +574,15 @@ void *malloc(size_t n0) { } // Bump-allocate from the high end. char *p = bumpPtr; - if (p + HDR_SZ + n > heapEnd) return (void *)0; + // Bounds check as a 16-bit SAME-BANK difference (avail = heapEnd - p), + // NOT `p + HDR_SZ + n > heapEnd`: that 24-bit pointer compare goes through + // the same i32 pattern the backend miscompiles at -O2 (see the size-round + // note above), so an oversized request returned a non-NULL OVER-HEAP + // pointer instead of NULL. On the tiny IIgs heap that let, e.g., a 16 KB + // scratch malloc "succeed" on a ~3 KB heap; the returned pointer + free() + // then trampled the IO window / language card / GS-OS stack and crashed. + // The heap lives in one bank, so the 16-bit offset difference is exact. + if ((u16)(heapEnd - p) < (u16)((u16)HDR_SZ + n)) return (void *)0; *(u16 *)p = (u16)n; bumpPtr = p + HDR_SZ + n; return p + HDR_SZ; diff --git a/src/link816/link816.cpp b/src/link816/link816.cpp index 4cb55ad..2e7d02f 100644 --- a/src/link816/link816.cpp +++ b/src/link816/link816.cpp @@ -711,7 +711,51 @@ static void applyTextReloc(std::vector &buf, uint32_t off, uint8_t rtype, const std::string &symName, uint32_t patchSeg, - const std::vector &segments) { + const std::vector &segments, + bool targetIsWeakNull = false) { + // UNDEFINED-WEAK reference resolved to absolute 0 (resolveSym's + // STB_WEAK branch). This is the `if (fp) fp();` null-stub pattern: + // e.g. libc's getchar() does `if (__getByte) return __getByte();` + // against the weak, link-time-unresolved __getByte console hook, so + // clang emits a 2-byte near JSR whose IMM16 operand is the hook's + // address -- 0 -- guarded by a null test that is dead at runtime. + // + // Two things force special handling here rather than the normal + // classification below: + // 1. The baked operand MUST stay literally 0 so the `if (fp)` test + // keeps failing. A cRELOC/cINTERSEG that rewrote those bytes to + // a placed segment base would make the null test pass and run the + // dead call into garbage. So we emit NO relocation record. + // 2. With --text-base 0 the null address (0) falls inside segment + // 1's [base, base+size) range, so findSegContaining() reports + // targetSeg == 1. A patch site in an overflow segment + // (patchSeg > 1) then looks like a cross-segment reference and + // the 2-byte IMM16/PCREL form is rejected further down (a near + // JSR has no room for the bank byte the Loader would need). With + // a non-zero --text-base the same reference resolves cleanly via + // the targetSeg == 0 path; baking the literal 0 here restores + // that behaviour regardless of the text base or the patch seg. + // + // Bake the literal little-endian value (0, or 0 + a weak addend) into + // the absolute/data operand and return with no side effects. PCREL + // forms (a branch to a null symbol) are nonsensical and never weak- + // null in practice, so they fall through to the normal path. + if (targetIsWeakNull) { + switch (rtype) { + case R_W65816_IMM8: + case R_W65816_IMM16: + case R_W65816_BANK16: + case R_W65816_IMM24: + case R_W65816_DATA32: { + uint32_t w = relocWidth(rtype); + for (uint32_t i = 0; i < w; ++i) + buf[off + i] = static_cast((target >> (8 * i)) & 0xFF); + return; + } + default: + break; + } + } if (!gMultiSegRelocMode) { applyReloc(buf, off, patchAddr, target, rtype, symName); return; @@ -987,9 +1031,17 @@ struct Linker { // referenced section is one we don't track (e.g. another .debug_* // section); strict callers should die() on false, lenient callers // (the DWARF sidecar) should leave the bytes object-local. + // `weakNull` (optional) reports whether the symbol was an UNDEFINED + // WEAK reference that ELF semantics resolve to absolute 0 (the + // `if (fp) fp();` null-stub pattern -- see the STB_WEAK branch below). + // The text-reloc patcher needs to know this because, with --text-base + // 0, that 0 collides with segment 1's bank-0 address range and would + // otherwise be mis-classified as a real cross-segment reference. bool resolveSym(const InputObject &obj, const ObjOffsets &oo, const Reloc &r, - uint32_t &target, std::string &resolvedName) const { + uint32_t &target, std::string &resolvedName, + bool *weakNull = nullptr) const { + if (weakNull) *weakNull = false; if (r.symIdx >= obj.symbols.size()) die(obj.path + ": reloc symIdx out of range"); const Symbol &sym = obj.symbols[r.symIdx]; @@ -1047,6 +1099,7 @@ struct Linker { resolvedName = sym.name; if (sym.bind == STB_WEAK) { target = 0 + r.addend; + if (weakNull) *weakNull = true; return true; } return false; @@ -1288,9 +1341,15 @@ struct Linker { if (!gTrampList.empty()) { gTrampBase = segSizes[0]; segSizes[0] += kTrampBytes * (uint32_t)gTrampList.size(); - if (segmentCap && segSizes[0] > segmentCap) - die("cross-segment trampoline table pushes seg 1 over " - "--segment-cap"); + // The trampolines are executable code appended to seg 1's text, + // so the HARD limit is the IO window ($C000), not the soft + // --segment-cap (a text-distribution target that leaves margin + // below the window). The small trampoline table may use that + // margin; an actual IO-window crossing is still caught here + // (and again by the textSize check below). + if (segSizes[0] > kIoWindowStart) + die("seg 1 text + cross-segment trampoline table crosses " + "the IIgs IO window 0xC000"); std::fprintf(stderr, "link816: %zu cross-seg function trampolines (%u bytes) " "in seg 1\n", gTrampList.size(), @@ -1545,6 +1604,14 @@ struct Linker { // constant. constexpr uint32_t kBank0HeapTop = kIoWindowStart - 0x100; // $BF00 constexpr uint32_t MIN_HEAP = 512; + // Prefer a bank-0 heap below the IO window. Pushing the heap into the + // bank-0 LC ($D000-$FFFF) is a LAST RESORT and unreliable: GS/OS uses + // that RAM, corrupting heap-resident state (a surface struct landing + // there had its palette read back as garbage -- both the $D000-$DFFF + // bank-switched half AND the $E000-$FFFF common half are unsafe). The + // driver (clang-build.sh) avoids it by lowering the multi-seg + // --segment-cap until rodata + BSS + heap fit below the IO window; this + // LC fallback only fires for a program too large for even that. uint32_t heapStart = L.bssBase + L.bssSize; if (heapStart >= kBank0HeapTop && heapStart < kIoWindowEnd) { heapStart = kIoWindowEnd; // skip IO window + tiny tail @@ -1619,17 +1686,22 @@ struct Linker { continue; } if (sym.bind == STB_LOCAL) { - // Locals get tracked separately under a per-object - // disambiguated key so writeMap() can list them - // with their provenance when --map-locals is on. - // The shared globalSyms path below still includes - // the local under its bare name for backwards- - // compat with smoke tests that grep the map for - // file-static names (e.g. ctor1). + // Per-object disambiguated key for writeMap()/--map-locals. std::string base = obj.path; size_t slash = base.find_last_of('/'); if (slash != std::string::npos) base = base.substr(slash + 1); localSyms[sym.name + "@" + base] = addr; + // Also expose under the bare name (FIRST-WINS) for smoke + // tests that grep the map for file-static names (e.g. + // ctor1) -- but do NOT enter the strong/weak resolution + // below: file-local statics are file-scoped, so the SAME + // static name in two TUs (e.g. uber.c and surface.c each + // with `static gStage`) is NOT a multiple-definition error. + // A real global of the same name still overrides this (the + // `sit == end` branch below replaces the bare entry). + if (globalSyms.find(sym.name) == globalSyms.end()) + globalSyms[sym.name] = addr; + continue; } bool thisStrong = (sym.bind != STB_WEAK); auto sit = isStrong.find(sym.name); @@ -1715,7 +1787,9 @@ struct Linker { uint32_t patchAddr = segBase + patchOff; uint32_t target; std::string resolvedName; - if (!resolveSym(obj, oo, r, target, resolvedName)) + bool targetWeakNull = false; + if (!resolveSym(obj, oo, r, target, resolvedName, + &targetWeakNull)) die(obj.path + ": .text reloc to unresolved '" + resolvedName + "'"); // PCREL relocs can't span banks (the displacement @@ -1725,8 +1799,12 @@ struct Linker { // bank, which we keep at 0 by default (so refs // to bank-0 data work from any code segment), // and we can't statically know the target bank - // intent anyway. - if (segmentCap && (r.type == R_W65816_PCREL16 || + // intent anyway. An undefined-weak null target + // (address 0) is exempt: it is not a real cross-bank + // reference, just the `if (fp) fp();` null sentinel, + // and applyTextReloc bakes the literal 0 for it. + if (segmentCap && !targetWeakNull && + (r.type == R_W65816_PCREL16 || r.type == R_W65816_PCREL8)) { uint32_t targetSegBank = target & 0xFF0000; uint32_t patchSegBank = segBase & 0xFF0000; @@ -1742,7 +1820,8 @@ struct Linker { } } applyTextReloc(textBuf, patchOff, patchAddr, target, - r.type, resolvedName, segNum, L.segments); + r.type, resolvedName, segNum, L.segments, + targetWeakNull); } } } @@ -1754,10 +1833,46 @@ struct Linker { lastLayout = L; // 4b. Apply relocations to rodata/data buffer. Globals like - // `int *p = &v;` need their initializer patched at link time - // (the .o emits a placeholder 0 + a R_W65816_IMM16 reloc). - // Without this, every initialized pointer or function-pointer - // table in the program reads 0 at runtime. + // `int *p = &v;` or `static const char *p = "x";` need their + // initializer patched (the .o emits a placeholder 0 + an + // R_W65816_IMM24 / R_W65816_DATA32 reloc — pointers are 24-bit + // far on this target, so a `char *` initializer is a 3-byte + // bank:offset value, not a 2-byte near offset). Without this, + // every initialized pointer or function-pointer table reads 0. + // + // This MUST go through applyTextReloc (NOT a bare applyReloc) so + // that, in the multi-segment ExpressLoad model, the pointer's + // BANK byte is patched at LOAD time the same way text-segment + // pointer operands are. The merged .data/.rodata image lives in + // segment 1 (the entry/load bank) — it is appended to seg 1's + // output image after the seg-1 text (see the outImage compose + // below) — so patchSeg is always 1 here. Routing through + // applyTextReloc lets the existing per-segment classifier fire: + // + // - target in .data/.rodata (outside every text seg's [base, + // base+textSize) range, so findSegContaining()==0): recorded + // as an intra-seg cRELOC in gIntraImm24SitesPerSeg[1]. The + // GS/OS Loader rewrites the 3 bytes to (seg1PlacedBase + + // offsetRef), supplying the runtime LOAD bank. This is the + // gCurName case: the link-time value is $00:offset but the + // image loads at e.g. bank $08, and only the cRELOC carries + // the $08 to the pointer's bank byte. + // - target in a seg2+ text bank (a function the layout placed + // in an overflow bank): recorded as a cINTERSEG so the Loader + // supplies that segment's placed bank. + // + // Before this change the .data path recorded only into the FLAT + // gImm24Sites list, which omfEmit consumes ONLY in single-segment + // mode; in --multi-seg-expressload it reads the per-seg sidecars + // (and suppresses the flat fallback for seg 1 once that per-seg + // list is non-empty from the text relocs), so every .data pointer + // initializer silently kept its link-time bank ($00) and crashed + // the moment it was dereferenced from the real load bank. + // + // Outside multi-seg mode applyTextReloc is a thin wrapper that + // delegates straight to applyReloc, so the single-segment path + // (and its flat gImm24Sites cRELOC list) is byte-for-byte + // unchanged. for (size_t fi = 0; fi < objs.size(); ++fi) { const auto &obj = *objs[fi]; const auto &oo = objOff[fi]; @@ -1771,11 +1886,16 @@ struct Linker { uint32_t patchAddr = L.rodataBase + patchOff; uint32_t target; std::string resolvedName; - if (!resolveSym(obj, oo, r, target, resolvedName)) + bool targetWeakNull = false; + if (!resolveSym(obj, oo, r, target, resolvedName, + &targetWeakNull)) die(obj.path + ": .rodata reloc to unresolved '" + resolvedName + "'"); - applyReloc(rodataBuf, patchOff, patchAddr, target, - r.type, resolvedName); + // patchSeg = 1: the merged rodata/.data image is part + // of segment 1's output image (text || gap || rodata). + applyTextReloc(rodataBuf, patchOff, patchAddr, target, + r.type, resolvedName, /*patchSeg=*/1, + L.segments, targetWeakNull); } } }