Register allocation and BSS > 64k fixed. Maybe.

This commit is contained in:
Scott Duensing 2026-06-28 17:59:50 -05:00
parent eabfaf3fcb
commit b3c5b9b241
2 changed files with 152 additions and 24 deletions

View file

@ -574,7 +574,15 @@ void *malloc(size_t n0) {
} }
// Bump-allocate from the high end. // Bump-allocate from the high end.
char *p = bumpPtr; 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; *(u16 *)p = (u16)n;
bumpPtr = p + HDR_SZ + n; bumpPtr = p + HDR_SZ + n;
return p + HDR_SZ; return p + HDR_SZ;

View file

@ -711,7 +711,51 @@ static void applyTextReloc(std::vector<uint8_t> &buf, uint32_t off,
uint8_t rtype, uint8_t rtype,
const std::string &symName, const std::string &symName,
uint32_t patchSeg, uint32_t patchSeg,
const std::vector<TextSeg> &segments) { const std::vector<TextSeg> &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<uint8_t>((target >> (8 * i)) & 0xFF);
return;
}
default:
break;
}
}
if (!gMultiSegRelocMode) { if (!gMultiSegRelocMode) {
applyReloc(buf, off, patchAddr, target, rtype, symName); applyReloc(buf, off, patchAddr, target, rtype, symName);
return; return;
@ -987,9 +1031,17 @@ struct Linker {
// referenced section is one we don't track (e.g. another .debug_* // referenced section is one we don't track (e.g. another .debug_*
// section); strict callers should die() on false, lenient callers // section); strict callers should die() on false, lenient callers
// (the DWARF sidecar) should leave the bytes object-local. // (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, bool resolveSym(const InputObject &obj, const ObjOffsets &oo,
const Reloc &r, 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()) if (r.symIdx >= obj.symbols.size())
die(obj.path + ": reloc symIdx out of range"); die(obj.path + ": reloc symIdx out of range");
const Symbol &sym = obj.symbols[r.symIdx]; const Symbol &sym = obj.symbols[r.symIdx];
@ -1047,6 +1099,7 @@ struct Linker {
resolvedName = sym.name; resolvedName = sym.name;
if (sym.bind == STB_WEAK) { if (sym.bind == STB_WEAK) {
target = 0 + r.addend; target = 0 + r.addend;
if (weakNull) *weakNull = true;
return true; return true;
} }
return false; return false;
@ -1288,9 +1341,15 @@ struct Linker {
if (!gTrampList.empty()) { if (!gTrampList.empty()) {
gTrampBase = segSizes[0]; gTrampBase = segSizes[0];
segSizes[0] += kTrampBytes * (uint32_t)gTrampList.size(); segSizes[0] += kTrampBytes * (uint32_t)gTrampList.size();
if (segmentCap && segSizes[0] > segmentCap) // The trampolines are executable code appended to seg 1's text,
die("cross-segment trampoline table pushes seg 1 over " // so the HARD limit is the IO window ($C000), not the soft
"--segment-cap"); // --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, std::fprintf(stderr,
"link816: %zu cross-seg function trampolines (%u bytes) " "link816: %zu cross-seg function trampolines (%u bytes) "
"in seg 1\n", gTrampList.size(), "in seg 1\n", gTrampList.size(),
@ -1545,6 +1604,14 @@ struct Linker {
// constant. // constant.
constexpr uint32_t kBank0HeapTop = kIoWindowStart - 0x100; // $BF00 constexpr uint32_t kBank0HeapTop = kIoWindowStart - 0x100; // $BF00
constexpr uint32_t MIN_HEAP = 512; 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; uint32_t heapStart = L.bssBase + L.bssSize;
if (heapStart >= kBank0HeapTop && heapStart < kIoWindowEnd) { if (heapStart >= kBank0HeapTop && heapStart < kIoWindowEnd) {
heapStart = kIoWindowEnd; // skip IO window + tiny tail heapStart = kIoWindowEnd; // skip IO window + tiny tail
@ -1619,17 +1686,22 @@ struct Linker {
continue; continue;
} }
if (sym.bind == STB_LOCAL) { if (sym.bind == STB_LOCAL) {
// Locals get tracked separately under a per-object // Per-object disambiguated key for writeMap()/--map-locals.
// 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).
std::string base = obj.path; std::string base = obj.path;
size_t slash = base.find_last_of('/'); size_t slash = base.find_last_of('/');
if (slash != std::string::npos) base = base.substr(slash + 1); if (slash != std::string::npos) base = base.substr(slash + 1);
localSyms[sym.name + "@" + base] = addr; 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); bool thisStrong = (sym.bind != STB_WEAK);
auto sit = isStrong.find(sym.name); auto sit = isStrong.find(sym.name);
@ -1715,7 +1787,9 @@ struct Linker {
uint32_t patchAddr = segBase + patchOff; uint32_t patchAddr = segBase + patchOff;
uint32_t target; uint32_t target;
std::string resolvedName; 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 '" die(obj.path + ": .text reloc to unresolved '"
+ resolvedName + "'"); + resolvedName + "'");
// PCREL relocs can't span banks (the displacement // PCREL relocs can't span banks (the displacement
@ -1725,8 +1799,12 @@ struct Linker {
// bank, which we keep at 0 by default (so refs // bank, which we keep at 0 by default (so refs
// to bank-0 data work from any code segment), // to bank-0 data work from any code segment),
// and we can't statically know the target bank // and we can't statically know the target bank
// intent anyway. // intent anyway. An undefined-weak null target
if (segmentCap && (r.type == R_W65816_PCREL16 || // (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)) { r.type == R_W65816_PCREL8)) {
uint32_t targetSegBank = target & 0xFF0000; uint32_t targetSegBank = target & 0xFF0000;
uint32_t patchSegBank = segBase & 0xFF0000; uint32_t patchSegBank = segBase & 0xFF0000;
@ -1742,7 +1820,8 @@ struct Linker {
} }
} }
applyTextReloc(textBuf, patchOff, patchAddr, target, 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; lastLayout = L;
// 4b. Apply relocations to rodata/data buffer. Globals like // 4b. Apply relocations to rodata/data buffer. Globals like
// `int *p = &v;` need their initializer patched at link time // `int *p = &v;` or `static const char *p = "x";` need their
// (the .o emits a placeholder 0 + a R_W65816_IMM16 reloc). // initializer patched (the .o emits a placeholder 0 + an
// Without this, every initialized pointer or function-pointer // R_W65816_IMM24 / R_W65816_DATA32 reloc — pointers are 24-bit
// table in the program reads 0 at runtime. // 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) { for (size_t fi = 0; fi < objs.size(); ++fi) {
const auto &obj = *objs[fi]; const auto &obj = *objs[fi];
const auto &oo = objOff[fi]; const auto &oo = objOff[fi];
@ -1771,11 +1886,16 @@ struct Linker {
uint32_t patchAddr = L.rodataBase + patchOff; uint32_t patchAddr = L.rodataBase + patchOff;
uint32_t target; uint32_t target;
std::string resolvedName; 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 '" die(obj.path + ": .rodata reloc to unresolved '"
+ resolvedName + "'"); + resolvedName + "'");
applyReloc(rodataBuf, patchOff, patchAddr, target, // patchSeg = 1: the merged rodata/.data image is part
r.type, resolvedName); // of segment 1's output image (text || gap || rodata).
applyTextReloc(rodataBuf, patchOff, patchAddr, target,
r.type, resolvedName, /*patchSeg=*/1,
L.segments, targetWeakNull);
} }
} }
} }