From 09c99937e763f6fa281e2b9190d4f22d2c644885 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Wed, 24 Jun 2026 19:23:30 -0500 Subject: [PATCH] Multisegment OMF --- link816.cpp | 0 patches/0008-elf-add-em-w65816.patch | 17 + scripts/smokeTest.sh | 175 +++++++++ src/link816/link816.cpp | 314 ++++++++++++++- src/link816/omfEmit.cpp | 369 ++++++++++++++---- .../lib/Target/W65816/W65816AsmPrinter.cpp | 83 +++- 6 files changed, 863 insertions(+), 95 deletions(-) create mode 100644 link816.cpp create mode 100644 patches/0008-elf-add-em-w65816.patch diff --git a/link816.cpp b/link816.cpp new file mode 100644 index 0000000..e69de29 diff --git a/patches/0008-elf-add-em-w65816.patch b/patches/0008-elf-add-em-w65816.patch new file mode 100644 index 0000000..7052472 --- /dev/null +++ b/patches/0008-elf-add-em-w65816.patch @@ -0,0 +1,17 @@ +diff --git a/llvm/include/llvm/BinaryFormat/ELF.h b/llvm/include/llvm/BinaryFormat/ELF.h +index b84b4e84f..4e45ece44 100644 +--- a/llvm/include/llvm/BinaryFormat/ELF.h ++++ b/llvm/include/llvm/BinaryFormat/ELF.h +@@ -326,6 +326,12 @@ enum { + EM_CSKY = 252, // C-SKY 32-bit processor + EM_LOONGARCH = 258, // LoongArch + EM_MOS = 6502, // MOS Technologies 65xx ++ EM_W65816 = 0xFF16, // WDC 65C816 / Apple IIgs (vendor-private, see ++ // docs/USAGE.md "ELF e_machine value" section). ++ // Sits in the 0xFF00-0xFFFF experimental/vendor ++ // range reserved by the ELF spec; the "16" suffix ++ // is mnemonic for "65816". Picked here because ++ // 65816 itself (0x10118) does not fit Elf32_Half. + }; + + // Object file classes. diff --git a/scripts/smokeTest.sh b/scripts/smokeTest.sh index dba0d65..5eae04a 100755 --- a/scripts/smokeTest.sh +++ b/scripts/smokeTest.sh @@ -5360,6 +5360,129 @@ EOF rm -f "$cMsegFile" "$oMsegFile" "$binMseg" "$mfMseg" \ "${binMseg%.bin}".seg*.bin + # Phase A: per-segment placement attribute. A function tagged + # __attribute__((section(".text.seg2"))) lands in seg 2 (bank- + # aligned at the default segmentBankBase 0x040000), not greedy- + # packed into seg 1. The main() in seg 1 calls into seg 2 via + # a cross-bank JSL; the value round-trips through pinnedFn and + # arrives at the check address. + log "check: link816 honors __attribute__((section(\".text.seg2\")))" + cPinFile="$(mktemp --suffix=.c)" + oPinFile="$(mktemp --suffix=.o)" + binPin="$(mktemp --suffix=.bin)" + mfPin="$(mktemp --suffix=.json)" + cat > "$cPinFile" <<'EOF' +__attribute__((section(".text.seg2"), noinline)) +int pinnedFn(int x) { return x * 7 + 3; } +volatile int gIn = 11; +int main(void) { + int r = pinnedFn(gIn); + *(volatile unsigned short *)0x025000 = (unsigned short)r; + while (1) {} +} +EOF + "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cPinFile" -o "$oPinFile" + "$PROJECT_ROOT/tools/link816" -o "$binPin" --text-base 0x1000 \ + --manifest "$mfPin" \ + "$oCrt0F" "$oLibgccFile" "$oPinFile" >/dev/null 2>&1 + if ! grep -q '"num": 2' "$mfPin"; then + die "Phase A: .text.seg2 did not produce a seg 2 in the manifest" + fi + if ! grep -q '"base": "0x040000"' "$mfPin"; then + die "Phase A: seg 2 base should be 0x040000 (segmentBankBase default)" + fi + if ! bash "$PROJECT_ROOT/scripts/runMultiSeg.sh" "$mfPin" --check \ + 0x025000=0050 /dev/null 2>&1; then + die "MAME: Phase A pinnedFn(11) cross-bank JSL != 0x50" + fi + rm -f "$cPinFile" "$oPinFile" "$binPin" "$mfPin" \ + "${binPin%.bin}".seg*.bin + + # Phase B: opt-in far globals. Globals declared via + # __attribute__((section(".rodata.far"))) / .data.far / .bss.far + # get LDA_Long/STA_Long codegen (bank-explicit), so they read + # correctly even when DBR points at a different bank. Layout: + # rodata in bank 0, BSS in bank 2 (--bss-base 0x020000); crt0 + # sets DBR=2. A short-form LDA on the rodata symbol would read + # bank 2:offset (garbage); the long-form reads bank-0:offset. + log "check: link816 + AsmPrinter Phase B (far globals cross-bank)" + cFarFile="$(mktemp --suffix=.c)" + oFarFile="$(mktemp --suffix=.o)" + binFar="$(mktemp --suffix=.bin)" + cat > "$cFarFile" <<'EOF' +__attribute__((section(".rodata.far"))) +const unsigned short gFarArr[3] = { 0xCAFE, 0xBABE, 0xF00D }; +__attribute__((section(".data.far"))) +volatile int gFarIdx = 1; +int main(void) { + *(volatile unsigned short *)0x025000 = gFarArr[gFarIdx]; + while (1) {} +} +EOF + "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cFarFile" -o "$oFarFile" + "$PROJECT_ROOT/tools/link816" -o "$binFar" --text-base 0x1000 \ + --bss-base 0x020000 "$oCrt0F" "$oLibgccFile" "$oFarFile" >/dev/null 2>&1 + if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binFar" --check \ + 0x025000=babe /dev/null 2>&1; then + die "MAME: Phase B far-global cross-bank read != 0xBABE" + fi + rm -f "$cFarFile" "$oFarFile" "$binFar" + + # Phase C.1: omfEmit --manifest --expressload wraps a multi-seg + # link816 manifest in a ~ExpressLoad OMF (KIND=0x1000 per user + # seg) instead of the static-bake KIND=0x8800 path. Verifies + # the structural output: 1 ExpressLoad seg + N user segs. + # Cross-segment JSL (Phase C.2 cINTERSEG) NOT yet implemented; + # cross-seg refs are still link-time-baked, so this OMF only + # works under runMultiSeg.sh's mini Lua loader (static placement + # honors link-time bases) — real GS/OS Loader launch needs C.2. + log "check: omfEmit --manifest --expressload produces wrapped multi-seg OMF" + cPhCFile="$(mktemp --suffix=.c)" + oPhCFile="$(mktemp --suffix=.o)" + binPhC="$(mktemp --suffix=.bin)" + mfPhC="$(mktemp --suffix=.json)" + omfPhC="$(mktemp --suffix=.omf)" + cat > "$cPhCFile" <<'EOF' +__attribute__((section(".text.seg2"), noinline)) +int pinnedFn(int x) { return x * 7 + 3; } +volatile int gIn = 11; +int main(void) { + *(volatile unsigned short *)0x025000 = (unsigned short)pinnedFn(gIn); + while (1) {} +} +EOF + "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cPhCFile" -o "$oPhCFile" + "$PROJECT_ROOT/tools/link816" -o "$binPhC" --text-base 0x1000 \ + --manifest "$mfPhC" \ + "$oCrt0F" "$oLibgccFile" "$oPhCFile" >/dev/null 2>&1 + "$PROJECT_ROOT/tools/omfEmit" --manifest "$mfPhC" --expressload \ + --output "$omfPhC" >/dev/null 2>&1 + # Verify exactly 3 segments and correct KINDs. + nPhCSeg=$(python3 -c " +import struct +data = open('$omfPhC','rb').read() +pos = 0; ok_kinds = [] +while pos < len(data): + bytecnt = struct.unpack_from('/dev/null 2>&1; then + die "MAME: Phase C.1 multi-seg .bin round-trip != 0x50" + fi + rm -f "$cPhCFile" "$oPhCFile" "$binPhC" "$mfPhC" "$omfPhC" \ + "${binPhC%.bin}".seg*.bin + rm -f "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" "$oCrt0F" else @@ -6394,6 +6517,58 @@ EOF "$omfGsf" "$testFileGsf" fi +# Phase C.2: cINTERSEG cross-segment JSL under real GS/OS Loader. +# Links a 2-user-seg program (main in seg 1, pinnedFn in seg 2 via +# __attribute__((section(".text.seg2")))), uses --multi-seg-expressload +# so link816 emits per-seg sidecars with the cross-seg JSL marked as +# inter (and zeros the patch bytes). omfEmit --manifest --expressload +# wraps in ExpressLoad with KIND=0x1000 per user seg and emits cINTERSEG +# (0xF6) opcodes so the Loader patches the JSL after picking banks +# dynamically. Under Finder launch the JSL must reach pinnedFn and the +# marker at $70 reads 0x50 = 11*7+3. Skips under SMOKE_SKIP_PHASEC2=1. +if [ "${SMOKE_SKIP_PHASEC2:-0}" != "1" ] \ + && [ -x "$CLANG" ] && [ -x "$CADIUS" ] && [ -f "$SYSDISK" ] \ + && command -v mame >/dev/null 2>&1; then + log "check: Phase C.2 cINTERSEG cross-seg JSL via GS/OS Loader" + cPhc2="$(mktemp --suffix=.c)" + oPhc2="$(mktemp --suffix=.o)" + binPhc2="$(mktemp --suffix=.bin)" + mapPhc2="$(mktemp --suffix=.map)" + relPhc2="$(mktemp --suffix=.reloc)" + mfPhc2="$(mktemp --suffix=.json)" + omfPhc2="$(mktemp --suffix=.omf)" + cat > "$cPhc2" <<'EOF' +__attribute__((section(".text.seg2"), noinline)) +int pinnedFn(int x) { return x * 7 + 3; } +volatile int gIn = 11; +int main(void) { + int r = pinnedFn(gIn); + *(volatile unsigned char *)0x70 = (unsigned char)r; + return 0; +} +EOF + "$CLANG" --target=w65816 -I"$PROJECT_ROOT/runtime/include" -O2 \ + -ffunction-sections -c "$cPhc2" -o "$oPhc2" + "$PROJECT_ROOT/tools/link816" -o "$binPhc2" --text-base 0x1000 \ + --bss-base 0xA000 --manifest "$mfPhc2" --map "$mapPhc2" \ + --reloc-out "$relPhc2" --multi-seg-expressload \ + "$PROJECT_ROOT/runtime/crt0Gsos.o" "$oPhc2" \ + "$PROJECT_ROOT/runtime/libgcc.o" >/dev/null 2>&1 \ + || die "Phase C.2: link failed" + "$PROJECT_ROOT/tools/omfEmit" --manifest "$mfPhc2" --expressload \ + --relocs "$relPhc2" --output "$omfPhc2" >/dev/null 2>&1 \ + || die "Phase C.2: omfEmit failed" + if ! bash "$PROJECT_ROOT/scripts/runViaFinder.sh" "$omfPhc2" \ + --check 0x70=0x50 >/dev/null 2>&1; then + bash "$PROJECT_ROOT/scripts/runViaFinder.sh" "$omfPhc2" \ + --check 0x70=0x50 2>&1 | tail -5 >&2 + die "Phase C.2: cINTERSEG cross-seg JSL marker != 0x50" + fi + rm -f "$cPhc2" "$oPhc2" "$binPhc2" "$mapPhc2" "$relPhc2" \ + "$mfPhc2" "$omfPhc2" \ + "${binPhc2%.bin}".seg*.bin "${relPhc2}".seg*.reloc +fi + # Stack-size end-to-end: omfEmit --stack-size must actually propagate a # larger DP/Stack chunk to the GS/OS Loader. Background: prior to the # 2026-06-02 fix, the ~Direct DP/Stack segment was appended to the OMF diff --git a/src/link816/link816.cpp b/src/link816/link816.cpp index ef75d0f..e4be5a3 100644 --- a/src/link816/link816.cpp +++ b/src/link816/link816.cpp @@ -188,6 +188,27 @@ static std::vector readFile(const std::string &path) { return buf; } +// Section-name parser for the per-segment placement attribute. Returns 0 if +// the section is unpinned (default greedy packing), or N >= 1 if the section +// is pinned to seg N. Convention: a text section whose name starts with +// `.text.segN` (decimal, 1-based) — directly or with a trailing dotted +// qualifier — pins that section's contents into segment N. Users author +// via `__attribute__((section(".text.seg2")))` or similar. Rationale: the +// existing sectionKind() classifier already treats `.text.` as +// the "text" kind, so the new convention costs nothing in the kind axis. +static uint32_t requestedSegment(const std::string &name) { + const std::string prefix = ".text.seg"; + if (name.rfind(prefix, 0) != 0) return 0; + size_t p = prefix.size(); + if (p >= name.size() || !std::isdigit((unsigned char)name[p])) return 0; + size_t e = p; + while (e < name.size() && std::isdigit((unsigned char)name[e])) ++e; + if (e < name.size() && name[e] != '.') return 0; // `.text.seg2foo` is not a pin + uint32_t n = static_cast(std::stoul(name.substr(p, e - p))); + return n; +} + + static std::string sectionKind(const std::string &name) { if (name == ".text" || name.rfind(".text.", 0) == 0) return "text"; if (name == ".rodata" || name.rfind(".rodata.", 0) == 0) return "rodata"; @@ -411,6 +432,31 @@ static std::vector gImm24Sites; static uint32_t gTextBaseForSites = 0; static bool gRecordSites = false; +// Phase C.2 — per-segment + cross-segment IMM24 site tracking, used +// when multi-segment ExpressLoad OMFs need the GS/OS Loader to patch +// cross-seg JSLs after picking banks dynamically. +// +// gIntraImm24SitesPerSeg[N] = sites within seg N whose target is also +// in seg N (cRELOC 0xF5 — Loader rewrites bytes to (segPlacedBase + +// offsetRef) when seg N lands). +// +// gInterImm24Sites = sites whose patch is in one seg but target lives +// in a different seg (cINTERSEG 0xF6 — Loader rewrites bytes to +// (targetSegPlacedBase + offsetRef) after picking banks for both +// segs). link816 bakes 0 at the patch site so the Loader's add does +// not double-count. +struct InterImm24Site { + uint16_t patchSeg; // 1-based seg containing the patch + uint16_t patchOff; // offset within patchSeg + uint8_t targetSeg; // 1-based seg containing the target + uint16_t targetOff; // offset within targetSeg + uint8_t byteCnt; // 3 = IMM24 (only kind supported for cINTERSEG today) + uint8_t bitShift; // 0 +}; +static std::map> gIntraImm24SitesPerSeg; +static std::vector gInterImm24Sites; +static bool gMultiSegRelocMode = false; // set when text layout has >1 segment + // Record an intra-segment patch site for cRELOC emission. A target // below the text base is never intra-segment (it is an undefined-weak @@ -431,6 +477,19 @@ static void recordCRelocSite(uint32_t patchAddr, uint32_t target, gImm24Sites.push_back(s); } +// Phase C.2 helper — find which text segment contains a 24-bit +// address. Returns the 1-based segNum, or 0 if the address is +// outside every text seg (e.g. rodata, bss, init_array, or an +// absolute address like an MMIO register). +static uint32_t findSegContaining(uint32_t addr, + const std::vector &segments) { + for (const auto &seg : segments) { + if (addr >= seg.base && addr < seg.base + seg.size) + return seg.segNum; + } + return 0; +} + // Number of bytes patched by a given reloc type. Used by callers // that need to range-check a reloc offset against a buffer size // without re-deriving the width inline. Returns 0 for unknown @@ -588,6 +647,110 @@ static void applyReloc(std::vector &buf, uint32_t off, } } +// Phase C.2 — text-section reloc patcher with per-seg / cross-seg +// classification. In multi-seg-reloc mode (gMultiSegRelocMode), an +// IMM24 site whose target lives in a different text segment is +// recorded as a cINTERSEG (Loader patches at load time after picking +// banks); the 3 bytes at the patch site are zeroed so the Loader's +// ADD computes (targetSegPlacedBase + targetOff) cleanly. Intra-seg +// IMM24/IMM16 sites still bake link-time-absolute bytes and are +// recorded as cRELOC (per-seg list). Sites whose target is outside +// every text segment (rodata / bss / init_array, or an undefined-weak +// resolving to 0) fall through to the legacy applyReloc path. +// +// Outside multi-seg-reloc mode, this is a thin wrapper that just +// calls applyReloc + recordCRelocSite via the legacy code path. +static void applyTextReloc(std::vector &buf, uint32_t off, + uint32_t patchAddr, uint32_t target, + uint8_t rtype, + const std::string &symName, + uint32_t patchSeg, + const std::vector &segments) { + if (!gMultiSegRelocMode) { + applyReloc(buf, off, patchAddr, target, rtype, symName); + return; + } + if (patchSeg == 0 || patchSeg > segments.size()) { + applyReloc(buf, off, patchAddr, target, rtype, symName); + return; + } + const TextSeg &pseg = segments[patchSeg - 1]; + uint32_t targetSeg = findSegContaining(target, segments); + // Non-text target (rodata / bss / init_array / weak-zero) — use + // the legacy applyReloc path (which records via recordCRelocSite + // into gImm24Sites for back-compat AND also pushes into the new + // per-seg list for seg 1, since rodata is in seg 1's bank by + // design). + if (targetSeg == 0) { + applyReloc(buf, off, patchAddr, target, rtype, symName); + // Also push into the per-seg list with the same gates as + // recordCRelocSite (target in same bank as textBase and + // >= textBase). Without these gates a target=0 reference + // (undefined-weak) would underflow offsetRef. + if (gRecordSites && + (rtype == R_W65816_IMM24 || rtype == R_W65816_DATA32 || + rtype == R_W65816_IMM16 || rtype == R_W65816_BANK16)) { + uint32_t targetBank = target & 0xFF0000; + uint32_t baseBank = gTextBaseForSites & 0xFF0000; + if (targetBank == baseBank && target >= gTextBaseForSites) { + Imm24Site s; + s.patchOff = patchAddr - pseg.base; + s.offsetRef = target - gTextBaseForSites; + s.byteCnt = (rtype == R_W65816_IMM24 || + rtype == R_W65816_DATA32) ? 3 : 2; + s.bitShift = (rtype == R_W65816_BANK16) ? 16 : 0; + gIntraImm24SitesPerSeg[patchSeg].push_back(s); + } + } + return; + } + // Cross-segment target: emit cINTERSEG. Currently only IMM24 + // (3-byte JSL/JML/STAlong/LDAlong/DATA32) is supported. IMM16 + // cross-seg refs can't be safely patched at load time (the bank + // byte the Loader could write doesn't fit), so they're rejected. + if (targetSeg != patchSeg) { + if (rtype != R_W65816_IMM24 && rtype != R_W65816_DATA32) { + char msg[200]; + std::snprintf(msg, sizeof(msg), + "cross-segment reloc to '%s' uses %u-byte form; only " + "IMM24 / DATA32 (3-byte JSL/JML/STAlong/LDAlong) supports " + "cINTERSEG patching under dynamic Loader placement", + symName.c_str(), relocWidth(rtype)); + die(msg); + } + // Zero the 3 bytes so the Loader's cINTERSEG ADD doesn't + // double-count. DATA32's high byte (the pad) stays untouched. + buf[off] = 0; + buf[off + 1] = 0; + buf[off + 2] = 0; + if (gRecordSites) { + const TextSeg &tseg = segments[targetSeg - 1]; + InterImm24Site s; + s.patchSeg = static_cast(patchSeg); + s.patchOff = static_cast(patchAddr - pseg.base); + s.targetSeg = static_cast(targetSeg); + s.targetOff = static_cast(target - tseg.base); + s.byteCnt = 3; + s.bitShift = 0; + gInterImm24Sites.push_back(s); + } + return; + } + // Intra-segment in multi-seg mode: bake bytes + record per-seg + // cRELOC. Do the byte write via applyReloc (which handles the + // type-specific layout) then push into the per-seg list. + applyReloc(buf, off, patchAddr, target, rtype, symName); + if (gRecordSites && (rtype == R_W65816_IMM24 || rtype == R_W65816_DATA32 || + rtype == R_W65816_IMM16 || rtype == R_W65816_BANK16)) { + Imm24Site s; + s.patchOff = patchAddr - pseg.base; + s.offsetRef = target - pseg.base; + s.byteCnt = (rtype == R_W65816_IMM24 || rtype == R_W65816_DATA32) ? 3 : 2; + s.bitShift = (rtype == R_W65816_BANK16) ? 16 : 0; + gIntraImm24SitesPerSeg[patchSeg].push_back(s); + } +} + struct Linker { std::vector> objs; uint32_t textBase = 0x8000; @@ -601,6 +764,13 @@ struct Linker { uint32_t segmentCap = 0; uint32_t segmentBankBase = 0x040000; std::string manifestPath; + // Phase C.2 — opt-in for ExpressLoad-multi-seg cINTERSEG emission. + // When true AND --reloc-out is set, link816 zeros cross-seg IMM24 + // patch sites and records them in gInterImm24Sites for omfEmit to + // emit as cINTERSEG (0xF6) opcodes. When false, cross-seg IMM24 + // sites stay link-time-absolute (the existing static-multi-seg + // behaviour used by runMultiSeg.sh). + bool multiSegExpressLoad = false; // ProDOS file metadata for the resulting OMF. Set via --filetype / // --aux. Used by disk-image builders (e.g. cppo, Cadius) to set // the on-disk filetype/aux when copying the .bin onto a 2mg. @@ -833,29 +1003,74 @@ struct Linker { std::vector segSizes = {0}; // bytes packed into each segment (1-based; index 0 = seg 1) uint32_t curSeg = 1; computeLiveSet(); + + // Pass 1: place sections that carry an explicit `.text.segN` pin + // (Phase A — per-segment placement attribute). Each pinned + // section is placed directly into segSizes[N-1], growing the + // segSizes vector as needed; the section's textBaseInMerged is + // accumulated in source-order alongside non-pinned sections so + // intra-object section ordering stays stable for `__text_*` + // boundary symbols. + auto growToSeg = [&](uint32_t seg) { + while (segSizes.size() < seg) segSizes.push_back(0); + }; for (size_t fi = 0; fi < objs.size(); ++fi) { ObjOffsets &oo = objOff[fi]; oo.textBaseInMerged = curText; for (uint32_t idx : objs[fi]->sectionsByKind("text")) { if (!isLive(fi, idx)) continue; uint32_t sz = objs[fi]->sections[idx].size; - // If adding this section would exceed the cap, start a - // new segment. Skip empty sections in the cap check - // (they fit anywhere). Sections larger than the cap - // get their own segment (we don't split a single - // section across banks — it'd violate intra-section - // PCREL and 16-bit absolute addressing). - if (segmentCap && sz > 0 && - segSizes[curSeg - 1] > 0 && - segSizes[curSeg - 1] + sz > segmentCap) { - curSeg++; + uint32_t wanted = requestedSegment(objs[fi]->sections[idx].name); + if (wanted == 0) continue; // unpinned — pass 2 handles it + growToSeg(wanted); + oo.textSegOf[idx] = wanted; + oo.textWithin[idx] = segSizes[wanted - 1]; + segSizes[wanted - 1] += sz; + curText += sz; + if (segmentCap && segSizes[wanted - 1] > segmentCap) { + char msg[200]; + std::snprintf(msg, sizeof(msg), + "pinned text in seg %u exceeds --segment-cap %u " + "(have %u bytes pinned, cap %u)", + wanted, segmentCap, + segSizes[wanted - 1], segmentCap); + die(msg); + } + } + } + + // Pass 2: greedy-pack non-pinned text sections. When the cap is + // exceeded — or when curSeg's slot is already full from pinned + // content — advance to the next slot with room. Empty sections + // fit anywhere; sections larger than the cap get their own seg + // (we don't split a single section across banks — it'd violate + // intra-section PCREL and 16-bit absolute addressing). + for (size_t fi = 0; fi < objs.size(); ++fi) { + ObjOffsets &oo = objOff[fi]; + for (uint32_t idx : objs[fi]->sectionsByKind("text")) { + if (!isLive(fi, idx)) continue; + if (requestedSegment(objs[fi]->sections[idx].name) != 0) continue; + uint32_t sz = objs[fi]->sections[idx].size; + if (sz > 0) { + while (true) { + if (curSeg > segSizes.size()) segSizes.push_back(0); + if (segSizes[curSeg - 1] == 0) break; + if (!segmentCap) break; + if (segSizes[curSeg - 1] + sz <= segmentCap) break; + curSeg++; + } + } else if (curSeg > segSizes.size()) { segSizes.push_back(0); } - oo.textSegOf[idx] = curSeg; + oo.textSegOf[idx] = curSeg; oo.textWithin[idx] = segSizes[curSeg - 1]; segSizes[curSeg - 1] += sz; curText += sz; } + } + + for (size_t fi = 0; fi < objs.size(); ++fi) { + ObjOffsets &oo = objOff[fi]; oo.rodataBaseInMerged = curRodata; for (uint32_t idx : objs[fi]->sectionsByKind("rodata")) { if (!isLive(fi, idx)) continue; @@ -1295,8 +1510,8 @@ struct Linker { die(msg); } } - applyReloc(textBuf, patchOff, patchAddr, target, r.type, - resolvedName); + applyTextReloc(textBuf, patchOff, patchAddr, target, + r.type, resolvedName, segNum, L.segments); } } } @@ -1723,6 +1938,9 @@ int main(int argc, char **argv) { } else if (a == "--manifest") { if (++i >= argc) usage(argv[0]); linker.manifestPath = argv[i++]; + } else if (a == "--multi-seg-expressload") { + linker.multiSegExpressLoad = true; + i++; } else if (a == "--filetype") { if (++i >= argc) usage(argv[0]); linker.fileType = (int32_t)parseInt(argv[i++]); @@ -1750,7 +1968,17 @@ int main(int argc, char **argv) { gRecordSites = true; gTextBaseForSites = linker.textBase; gImm24Sites.clear(); + gIntraImm24SitesPerSeg.clear(); + gInterImm24Sites.clear(); } + // Phase C.2 — multi-seg-relocation mode is opt-in via + // `--multi-seg-expressload`. It changes the cross-segment IMM24 + // bake from "link-time absolute address" (the static-multi-seg + // default consumed by runMultiSeg.sh's Lua loader) to "zero bytes + // + cINTERSEG sidecar entry" (consumed by omfEmit --manifest + // --expressload, which emits cINTERSEG 0xF6 opcodes so the GS/OS + // Loader patches cross-seg JSLs after picking banks dynamically). + gMultiSegRelocMode = linker.multiSegExpressLoad; std::vector image; Layout L = linker.link(image); @@ -1777,7 +2005,7 @@ int main(int argc, char **argv) { if (!mapPath.empty()) linker.writeMap(mapPath); if (!debugOutPath.empty()) linker.writeDebugSidecar(debugOutPath); if (!relocOutPath.empty()) { - // Sidecar binary format (v3): + // Single-flat sidecar binary format (v3): // u32 count // { u32 patchOff; u32 offsetRef; u8 byteCnt; u8 bitShift; // u8 pad[2]; } × count @@ -1787,6 +2015,24 @@ int main(int argc, char **argv) { // with the bank byte instead of the offset). v2 used pad[0]=0 // for byteCnt 2/3, so v3 is backwards-compatible: pad[0] was // always 0 then, equivalent to bitShift=0. + // + // Phase C.2 — under --multi-seg-expressload, we instead write a + // per-segment sidecar file alongside the main one: + // .seg.reloc + // Each per-seg file uses v4 format: + // u32 intraCount + // u32 interCount + // intraCount × 12B (same v3 layout, offsets within this seg) + // interCount × 12B inter entries: + // u32 patchOff (in this seg) + // u32 targetOff (in target seg) + // u8 targetSeg + // u8 byteCnt + // u8 bitShift + // u8 pad + // omfEmit reads the per-seg files when --manifest+--expressload + // is in effect; otherwise it reads the flat file (single-seg + // back-compat). std::ofstream rf(relocOutPath, std::ios::binary); if (!rf) die("cannot open '" + relocOutPath + "' for writing"); uint32_t count = (uint32_t)gImm24Sites.size(); @@ -1800,6 +2046,46 @@ int main(int argc, char **argv) { rf.write(reinterpret_cast(&bs), 1); rf.write(reinterpret_cast(pad), 2); } + if (linker.multiSegExpressLoad) { + for (size_t k = 0; k < L.segments.size(); k++) { + uint32_t segNum = L.segments[k].segNum; + char perSegPath[512]; + std::snprintf(perSegPath, sizeof(perSegPath), + "%s.seg%u.reloc", relocOutPath.c_str(), segNum); + std::ofstream pf(perSegPath, std::ios::binary); + if (!pf) die(std::string("cannot open '") + perSegPath + + "' for writing"); + const auto &intra = gIntraImm24SitesPerSeg[segNum]; + uint32_t interForSeg = 0; + for (const auto &s : gInterImm24Sites) { + if (s.patchSeg == segNum) interForSeg++; + } + uint32_t intraCount = (uint32_t)intra.size(); + pf.write(reinterpret_cast(&intraCount), 4); + pf.write(reinterpret_cast(&interForSeg), 4); + for (const auto &s : intra) { + uint32_t po = s.patchOff, off = s.offsetRef; + uint8_t bc = s.byteCnt, bs = s.bitShift, pad[2] = {0, 0}; + pf.write(reinterpret_cast(&po), 4); + pf.write(reinterpret_cast(&off), 4); + pf.write(reinterpret_cast(&bc), 1); + pf.write(reinterpret_cast(&bs), 1); + pf.write(reinterpret_cast(pad), 2); + } + for (const auto &s : gInterImm24Sites) { + if (s.patchSeg != segNum) continue; + uint32_t po = s.patchOff; + uint32_t to = s.targetOff; + uint8_t ts = s.targetSeg, bc = s.byteCnt, bs = s.bitShift, pad = 0; + pf.write(reinterpret_cast(&po), 4); + pf.write(reinterpret_cast(&to), 4); + pf.write(reinterpret_cast(&ts), 1); + pf.write(reinterpret_cast(&bc), 1); + pf.write(reinterpret_cast(&bs), 1); + pf.write(reinterpret_cast(&pad), 1); + } + } + } } // Multi-segment: write per-segment images + manifest if there's // more than one segment OR --manifest was requested. diff --git a/src/link816/omfEmit.cpp b/src/link816/omfEmit.cpp index 59c8ea2..dc09215 100644 --- a/src/link816/omfEmit.cpp +++ b/src/link816/omfEmit.cpp @@ -38,6 +38,7 @@ namespace { static constexpr uint8_t OMF_OP_LCONST = 0xF2; static constexpr uint8_t OMF_OP_CRELOC = 0xF5; static constexpr uint8_t OMF_OP_END = 0x00; +static constexpr uint8_t OMF_OP_CINTERSEG = 0xF6; [[maybe_unused]] static constexpr uint8_t OMF_NUMLEN = 4; [[maybe_unused]] static constexpr uint8_t OMF_VERSION_V21 = 0x02; [[maybe_unused]] static constexpr uint32_t OMF_HDR_SIZE = 44; @@ -68,6 +69,21 @@ struct RelocSite { }; std::vector gReloc24Sites; +// Phase C.2 — inter-segment IMM24 site for cINTERSEG (0xF6) emission. +// Populated from link816's per-seg sidecar (`.seg.reloc`) +// when --manifest+--expressload is in effect. +struct InterRelocSite { + uint16_t patchOff; // in this segment + uint16_t targetOff; // in target segment + uint8_t targetSeg; // 1-based target seg num (within this OMF) + uint8_t byteCnt; // 3 = IMM24 + uint8_t bitShift; // 0 +}; + +// cINTERSEG opcode wire size: 0xF6 + ByteCnt + BitShift + OffsetPatch +// + SegNum + OffsetReference = 1 + 1 + 1 + 2 + 1 + 2 = 8 bytes per site. +static constexpr uint32_t OMF_CINTERSEG_BYTES_PER_SITE = 8; + static std::vector readFile(const std::string &path) { std::ifstream f(path, std::ios::binary); if (!f) die("cannot open '" + path + "' for reading"); @@ -122,6 +138,18 @@ static void put16(std::vector &v, uint16_t x) { // `kind` : OMF KIND field. Caller picks; v1 uses 0x8800 (STATIC | // ABSBANK | CODE) for multi-segment static placement, or // 0x0000 (CODE, dynamic) for single-segment legacy mode. +static std::vector emitOneSeg(const std::vector &image, + uint32_t entryOff, + uint32_t org, + uint16_t segNum, + uint16_t kind, + const std::string &name, + uint32_t bssSize, + uint32_t bssGap, + const std::vector &relocSites, + const std::vector &interSites); + +// Adapter: legacy single-seg call sites that read from `gReloc24Sites`. static std::vector emitOneSeg(const std::vector &image, uint32_t entryOff, uint32_t org, @@ -130,6 +158,21 @@ static std::vector emitOneSeg(const std::vector &image, const std::string &name, uint32_t bssSize = 0, uint32_t bssGap = 0) { + static const std::vector noInter; + return emitOneSeg(image, entryOff, org, segNum, kind, name, + bssSize, bssGap, gReloc24Sites, noInter); +} + +static std::vector emitOneSeg(const std::vector &image, + uint32_t entryOff, + uint32_t org, + uint16_t segNum, + uint16_t kind, + const std::string &name, + uint32_t bssSize, + uint32_t bssGap, + const std::vector &relocSites, + const std::vector &interSites) { std::vector body; // Combined image: caller's LCONST data + zero-padding to bss-start // offset + bssSize zero bytes. We embed BSS-as-zeros in the LCONST @@ -166,13 +209,33 @@ static std::vector emitOneSeg(const std::vector &image, // (segPlacedBase + OffsetReference) at load time. This is what // makes JSL/JML/STAlong/etc. with intra-segment targets work when // the Loader places us at non-zero bank. - for (const auto &s : gReloc24Sites) { + for (const auto &s : relocSites) { body.push_back(OMF_OP_CRELOC); body.push_back(s.byteCnt); // ByteCnt (2 or 3) body.push_back(s.bitShift); // BitShift (0 or 16) put16(body, s.patchOff); // OffsetPatch put16(body, s.offsetRef); // OffsetReference } + // cINTERSEG opcodes (0xF6) for cross-segment patch sites. Format + // per Apple GS/OS Toolbox Reference Vol 3, Appendix B: + // 1B 0xF6 opcode + // 1B ByteCnt (3 for IMM24 — the only width usable here) + // 1B BitShift (0) + // 2B OffsetPatch (in this segment) + // 1B SegNum (target seg number; cINTERSEG is single-file) + // 2B OffsetReference (in target seg) + // The Loader patches segment[OffsetPatch..OffsetPatch+ByteCnt-1] to + // (targetSegPlacedBase + OffsetReference) ADDED to the constant + // already there. link816 zeroes the patch bytes so the ADD becomes + // the bare resolved address — see applyTextReloc inter branch. + for (const auto &s : interSites) { + body.push_back(OMF_OP_CINTERSEG); + body.push_back(s.byteCnt); + body.push_back(s.bitShift); + put16(body, s.patchOff); + body.push_back(s.targetSeg); + put16(body, s.targetOff); + } body.push_back(OMF_OP_END); // END opcode // Real OMF format (Merlin32 convention, verified GS/OS Loader-launchable): @@ -404,6 +467,30 @@ static std::vector emitOMF(const std::vector &image, // +58.. : SEG_NAME (length-prefixed) // // All counts use NUMLEN=4 (4-byte length on LCONST opcodes). +// Phase C — multi-user-segment ExpressLoad descriptor. Each entry +// describes one user code segment (plus its optional BSS tail and +// per-segment cRELOC sites recorded by link816). ENTRY_OFFSET is only +// meaningful on the first user seg (= seg 2 of the OMF; the Loader +// JSLs there to start the program). bssSize/bssGap follow the same +// LCONST-embedded-zeros convention as emitOneSeg. relocSites is the +// intra-segment cRELOC list — cross-segment references (cINTERSEG) +// are NOT yet emitted; this descriptor is the prep for that work. +struct UserSeg { + std::vector image; + uint32_t entryOffset = 0; + std::string name; + uint32_t bssSize = 0; + uint32_t bssGap = 0; + std::vector relocSites; + std::vector interSites; // Phase C.2 cINTERSEG sites +}; + +static std::vector emitOmfExpressLoad( + const std::vector &users, + uint32_t stackSize = 0); + +// Legacy single-user-seg adapter. Reads cRELOC sites from the global +// `gReloc24Sites` (filled by --relocs). static std::vector emitOmfExpressLoad( const std::vector &image, uint32_t entryOffset, @@ -411,12 +498,35 @@ static std::vector emitOmfExpressLoad( uint32_t stackSize = 0, uint32_t bssSize = 0, uint32_t bssGap = 0) { + UserSeg u; + u.image = image; + u.entryOffset = entryOffset; + u.name = userSegName; + u.bssSize = bssSize; + u.bssGap = bssGap; + u.relocSites = gReloc24Sites; + return emitOmfExpressLoad(std::vector{u}, stackSize); +} - // Step 1: build the user segment using KIND=0x1000 (CODE|PRIV). - // Same KIND emitOMF uses for single-segment apps. Verified - // Loader-launchable via the Finder smoke path. - auto userSeg = emitOneSeg(image, entryOffset, /*org*/0, /*segNum*/2, - /*kind*/0x1000, userSegName, bssSize, bssGap); +static std::vector emitOmfExpressLoad( + const std::vector &users, + uint32_t stackSize) { + if (users.empty()) die("emitOmfExpressLoad: no user segments"); + + // Step 1: build every user segment using KIND=0x1000 (CODE|PRIV). + // Same KIND emitOMF uses for single-segment apps. Each user seg + // gets sequential SEGNUM starting at 2 (seg 1 is ~ExpressLoad). + // The optional DP/Stack seg gets the trailing SEGNUM. + std::vector> userSegs; + userSegs.reserve(users.size()); + for (size_t k = 0; k < users.size(); k++) { + const auto &u = users[k]; + userSegs.push_back(emitOneSeg(u.image, u.entryOffset, /*org*/0, + /*segNum*/(uint16_t)(k + 2), + /*kind*/OMF_KIND_CODE_PRIV, + u.name, u.bssSize, u.bssGap, + u.relocSites, u.interSites)); + } // Optionally build the DP/Stack segment. If present it lives in the // file AFTER the user seg and gets its own ExpressLoad segtable + @@ -463,35 +573,59 @@ static std::vector emitOmfExpressLoad( // uses LABLEN=0 (length-prefixed name): 1 length byte + 12 chars. const std::string elName = "~ExpressLoad"; const uint32_t elNameAreaSize = LOAD_NAME_SIZE + 1 + (uint32_t)elName.size(); - const uint32_t nSegs = haveDpStack ? 2 : 1; // non-ExpressLoad segs + // Total non-ExpressLoad segs = N user segs + optional DP/Stack. + // Rebuild dpStackSeg with the correct trailing segNum now that we + // know how many user segs there are. + if (haveDpStack) { + dpStackSeg = emitDpStackSeg(stackSize, + /*segNum*/(uint16_t)(users.size() + 2)); + } + const uint32_t nSegs = (uint32_t)users.size() + (haveDpStack ? 1u : 0u); const uint32_t elDataSize = HEADER_BYTES + (SEGTAB_ENTRY + REMAP_ENTRY + HDR_INFO_ENTRY) * nSegs; // Body size = 1 byte LCONST opcode + 4 byte length + data + 1 byte END const uint32_t elBodySize = 1 + 4 + elDataSize + 1; const uint32_t elSegSize = HDR_SIZE + elNameAreaSize + elBodySize; - // User segment file offsets (after ExpressLoad seg). - const uint32_t userSegStart = elSegSize; - const uint32_t userBodyOpOff = userSegStart + HDR_SIZE + userNameAreaSize; - const uint32_t userDataOff = userBodyOpOff + 5; // 1 op + 4 length + // User segment file offsets (cascade after ExpressLoad seg). For + // each user seg, dataOff = (segStart + HDR_SIZE + nameArea + 1B + // LCONST opcode + 4B length). + std::vector userDataOffs; + std::vector userCRelocOffs; + std::vector userSegStarts; + userDataOffs.reserve(users.size()); + userCRelocOffs.reserve(users.size()); + userSegStarts.reserve(users.size()); + uint32_t segStart = elSegSize; + for (size_t k = 0; k < users.size(); k++) { + userSegStarts.push_back(segStart); + const uint32_t bodyOpOff = segStart + HDR_SIZE + userNameAreaSize; + const uint32_t dataOff = bodyOpOff + 5; + userDataOffs.push_back(dataOff); + const uint32_t dataLen = (uint32_t)users[k].image.size() + + users[k].bssGap + users[k].bssSize; + userCRelocOffs.push_back(dataOff + dataLen); + segStart += (uint32_t)userSegs[k].size(); + } - // DP/Stack segment file offsets (after user seg). The DP/Stack body - // mirrors the real GNO/ME ~_STACK seg format: an LCONST opcode + 4 - // byte length + `stackSize` zero bytes + END. ExpressLoad's + // DP/Stack segment file offsets (after all user segs). The DP/Stack + // body mirrors the real GNO/ME ~_STACK seg format: an LCONST opcode + // + 4 byte length + `stackSize` zero bytes + END. ExpressLoad's // hdr_info entry has to point at the LCONST data so the Loader // copies the right number of zeros into the allocated chunk — a // body of just END (RESSPC-only) silently no-ops on the // ExpressLoad fast path, which is the bug this whole section fixes. - const uint32_t dpStackSegStart = userSegStart + (uint32_t)userSeg.size(); + const uint32_t dpStackSegStart = segStart; const uint32_t dpStackBodyOff = dpStackSegStart + HDR_SIZE + (LOAD_NAME_SIZE + SEG_NAME_SIZE); const uint32_t dpStackDataOff = dpStackBodyOff + 5; // 1 op + 4 length // Step 3: build the ExpressLoad LCONST data. std::vector elData; // Header (6 bytes): reserved DWORD + count WORD. count = N-2 where - // N = total segments in the file (including ExpressLoad). With a - // DP/Stack seg N=3 so count=1; without it N=2 so count=0. + // N = total segments in the file (including ExpressLoad). For a + // 1-user-seg layout count=0 (N=2) or count=1 (N=3 with DP/Stack); + // for K user segs count = K - 1 (+ 1 if DP/Stack). put32(elData, 0); // reserved - put16(elData, (uint16_t)(haveDpStack ? 1 : 0)); // count = N-2 + put16(elData, (uint16_t)(nSegs - 1)); // count = N-2 = nSegs-1 // Segment list: one 8-byte entry per non-ExpressLoad segment. Each // entry's first WORD is the SELF-RELATIVE offset (from this entry's @@ -508,52 +642,52 @@ static std::vector emitOmfExpressLoad( } // Remap list: 1 WORD per non-ExpressLoad seg, giving the new - // segment number for each old segment position. Old seg 1 (user - // code, would-be sole seg without ExpressLoad) → new seg 2. - // Old seg 2 (DP/Stack, only present when --stack-size) → new seg 3. - put16(elData, 2); - if (haveDpStack) { - put16(elData, 3); + // segment number for each old segment position. Each non-EL seg + // gets new SEGNUM = (positionInExpressLoad + 2), starting at 2 for + // the first user seg. DP/Stack lands last. + for (uint32_t i = 0; i < nSegs; i++) { + put16(elData, (uint16_t)(i + 2)); } - // Header info entry for the user segment. - // data length = LCONST data size in the file. emitOneSeg embeds - // bssGap bytes of zero padding + bssSize bytes of BSS-as-zeros in - // the LCONST after the caller's image, so the on-disk data is - // image.size() + bssGap + bssSize bytes. - put32(elData, userDataOff); // data offset in file - put32(elData, (uint32_t)image.size() + bssGap + bssSize); // data length - // cRELOC opcodes (if any) are emitted by emitOneSeg directly after - // the LCONST data and before the END opcode. Tell ExpressLoad - // where they live so the Loader can apply them — without this the - // Loader skips the body walk and intra-segment IMM24 references - // (every JSL into our text) point at bank 0 even after relocation. - if (gReloc24Sites.empty()) { - put32(elData, 0); // reloc offset - put32(elData, 0); // reloc length - } else { - const uint32_t crelocOff = - userDataOff + (uint32_t)image.size() + bssGap + bssSize; - const uint32_t crelocLen = - OMF_CRELOC_BYTES_PER_SITE * (uint32_t)gReloc24Sites.size(); - put32(elData, crelocOff); - put32(elData, crelocLen); - } - - // Header copy: bytes [12..43] of user segment header, DISPDATA → 0. - if (userSeg.size() < HDR_SIZE) die("internal: user seg too small"); - elData.insert(elData.end(), userSeg.begin() + 12, userSeg.begin() + HDR_SIZE); - // DISPDATA is at offset 42..43 of the original header; in the copy - // (which omits the first 12 bytes), it lands at offset 30..31. - elData[elData.size() - 32 + 30] = 0; - elData[elData.size() - 32 + 31] = 0; - - // LOAD_NAME (10 bytes, space-padded — matches Merlin convention) - for (int i = 0; i < (int)LOAD_NAME_SIZE; i++) elData.push_back(0x20); - // SEG_NAME (10 bytes fixed-width, space-padded) - std::string truncated = userSegName.substr(0, SEG_NAME_SIZE); - for (size_t i = 0; i < SEG_NAME_SIZE; i++) { - elData.push_back(i < truncated.size() ? (uint8_t)truncated[i] : 0x20); + // Header info entries — one per non-ExpressLoad segment. Each + // entry is 68 bytes: 16B (dataOff,dataLen,relocOff,relocLen) + 32B + // header copy + 10B LOAD_NAME + 10B SEG_NAME. data length = LCONST + // data size in the file. emitOneSeg embeds bssGap bytes of zero + // padding + bssSize bytes of BSS-as-zeros in the LCONST after the + // caller's image, so the on-disk data is image.size() + bssGap + + // bssSize bytes. cRELOC opcodes (if any) are emitted by emitOneSeg + // directly after the LCONST data and before the END opcode; tell + // ExpressLoad where they live so the Loader can apply them. + for (size_t k = 0; k < users.size(); k++) { + const auto &u = users[k]; + const auto &seg = userSegs[k]; + if (seg.size() < HDR_SIZE) die("internal: user seg too small"); + const uint32_t dataLen = (uint32_t)u.image.size() + u.bssGap + u.bssSize; + put32(elData, userDataOffs[k]); // data offset in file + put32(elData, dataLen); // data length + const uint32_t relocLen = + OMF_CRELOC_BYTES_PER_SITE * (uint32_t)u.relocSites.size() + + OMF_CINTERSEG_BYTES_PER_SITE * (uint32_t)u.interSites.size(); + if (relocLen == 0) { + put32(elData, 0); // reloc offset + put32(elData, 0); // reloc length + } else { + put32(elData, userCRelocOffs[k]); + put32(elData, relocLen); + } + // Header copy: bytes [12..43] of user segment header, DISPDATA → 0. + elData.insert(elData.end(), seg.begin() + 12, seg.begin() + HDR_SIZE); + // DISPDATA is at offset 42..43 of the original header; in the copy + // (which omits the first 12 bytes), it lands at offset 30..31. + elData[elData.size() - 32 + 30] = 0; + elData[elData.size() - 32 + 31] = 0; + // LOAD_NAME (10 bytes, space-padded — matches Merlin convention) + for (int i = 0; i < (int)LOAD_NAME_SIZE; i++) elData.push_back(0x20); + // SEG_NAME (10 bytes fixed-width, space-padded) + std::string truncated = u.name.substr(0, SEG_NAME_SIZE); + for (size_t i = 0; i < SEG_NAME_SIZE; i++) { + elData.push_back(i < truncated.size() ? (uint8_t)truncated[i] : 0x20); + } } // Header info entry for the DP/Stack segment (when present). @@ -627,14 +761,15 @@ static std::vector emitOmfExpressLoad( if (elSeg.size() != elSegSize) die("internal: ExpressLoad segment size mismatch"); - // Step 6: concatenate ExpressLoad + user segment + optional DP/Stack. + // Step 6: concatenate ExpressLoad + user segments + optional DP/Stack. // The DP/Stack seg's presence is now also recorded in the // ExpressLoad load script (segtable + remap + header_info entries // above) so the Loader's fast path honors KIND=0x4012 instead of // silently dropping it to its default 4 KB DP/Stack allocation. std::vector result; result.insert(result.end(), elSeg.begin(), elSeg.end()); - result.insert(result.end(), userSeg.begin(), userSeg.end()); + for (const auto &seg : userSegs) + result.insert(result.end(), seg.begin(), seg.end()); if (haveDpStack) { result.insert(result.end(), dpStackSeg.begin(), dpStackSeg.end()); } @@ -805,8 +940,9 @@ int main(int argc, char **argv) { die("--stack-size must be a multiple of 256 (page-aligned)"); if (stackSize > 0xFFFF) die("--stack-size cannot exceed 65535 bytes (one bank)"); - if (!manifest.empty()) - die("--stack-size with --manifest not yet supported"); + // --stack-size + --manifest is allowed when ExpressLoad wraps + // the multi-seg file (auto-enabled below); the DP/Stack seg + // gets the trailing SEGNUM after all user segs. // Plain (non-ExpressLoad) multi-segment OMFs do not launch // correctly under the GS/OS 6.0.2 Loader — verified empirically: // the bare DP/Stack + code combo is rejected (program never @@ -857,6 +993,107 @@ int main(int argc, char **argv) { // Multi-segment mode. if (!manifest.empty()) { auto segs = parseManifest(manifest); + // --manifest + --expressload: wrap N user segments in a single + // ~ExpressLoad descriptor (Phase C.1). Each user seg gets + // KIND=0x1000 (CODE|PRIV), so the Loader picks banks dynamically + // — programs MUST NOT depend on link-time bank placement of + // cross-segment IMM24 targets (those need cINTERSEG, deferred + // to Phase C.2). Intra-segment cRELOC is supported via the + // global gReloc24Sites (single-sidecar path; per-seg sidecars + // also deferred to Phase C.2). + if (expressload) { + std::vector users; + users.reserve(segs.size()); + // Phase C.2 — when link816 is invoked with + // --multi-seg-expressload --reloc-out FILE, it writes per-seg + // sidecars at FILE.seg.reloc with intra + inter site + // lists. Look for them next to the user-supplied relocFile. + for (size_t k = 0; k < segs.size(); ++k) { + const auto &s = segs[k]; + UserSeg u; + u.image = readFile(s.image); + u.entryOffset = (k == 0) ? s.entryOff : 0; + u.name = s.name; + if (!relocFile.empty()) { + char perSegPath[512]; + std::snprintf(perSegPath, sizeof(perSegPath), + "%s.seg%u.reloc", relocFile.c_str(), + s.num); + std::ifstream pf(perSegPath, std::ios::binary); + if (pf) { + std::vector raw( + (std::istreambuf_iterator(pf)), + std::istreambuf_iterator()); + if (raw.size() < 8) + die(std::string(perSegPath) + ": too small"); + uint32_t intraCount = (uint32_t)raw[0] + | ((uint32_t)raw[1] << 8) + | ((uint32_t)raw[2] << 16) + | ((uint32_t)raw[3] << 24); + uint32_t interCount = (uint32_t)raw[4] + | ((uint32_t)raw[5] << 8) + | ((uint32_t)raw[6] << 16) + | ((uint32_t)raw[7] << 24); + size_t want = 8 + 12 * (size_t)intraCount + + 12 * (size_t)interCount; + if (raw.size() != want) + die(std::string(perSegPath) + ": size mismatch"); + size_t off = 8; + for (uint32_t i = 0; i < intraCount; i++) { + uint32_t po = (uint32_t)raw[off] + | ((uint32_t)raw[off+1] << 8) + | ((uint32_t)raw[off+2] << 16) + | ((uint32_t)raw[off+3] << 24); + uint32_t orf = (uint32_t)raw[off+4] + | ((uint32_t)raw[off+5] << 8) + | ((uint32_t)raw[off+6] << 16) + | ((uint32_t)raw[off+7] << 24); + RelocSite r; + r.patchOff = (uint16_t)po; + r.offsetRef = (uint16_t)orf; + r.byteCnt = raw[off+8]; + r.bitShift = raw[off+9]; + u.relocSites.push_back(r); + off += 12; + } + for (uint32_t i = 0; i < interCount; i++) { + uint32_t po = (uint32_t)raw[off] + | ((uint32_t)raw[off+1] << 8) + | ((uint32_t)raw[off+2] << 16) + | ((uint32_t)raw[off+3] << 24); + uint32_t to = (uint32_t)raw[off+4] + | ((uint32_t)raw[off+5] << 8) + | ((uint32_t)raw[off+6] << 16) + | ((uint32_t)raw[off+7] << 24); + InterRelocSite is; + is.patchOff = (uint16_t)po; + is.targetOff = (uint16_t)to; + // Remap link816 seg N → OMF SEGNUM (N+1): + // ExpressLoad occupies OMF seg 1, so each + // user seg's OMF SEGNUM is link816 seg + 1. + is.targetSeg = (uint8_t)(raw[off+8] + 1); + is.byteCnt = raw[off+9]; + is.bitShift = raw[off+10]; + u.interSites.push_back(is); + off += 12; + } + } + } + // Back-compat for layouts without per-seg sidecars: seg + // 1 inherits the global --relocs flat list. + if (u.relocSites.empty() && u.interSites.empty() && k == 0) + u.relocSites = gReloc24Sites; + users.push_back(std::move(u)); + } + auto blob = emitOmfExpressLoad(users, stackSize); + std::ofstream f(output, std::ios::binary); + if (!f) die("cannot open '" + output + "' for writing"); + f.write(reinterpret_cast(blob.data()), blob.size()); + std::fprintf(stderr, + "OMF: ExpressLoad + %zu user segments -> %s (%zu bytes total)\n", + segs.size(), output.c_str(), blob.size()); + return 0; + } std::vector blob; size_t totalPayload = 0; for (size_t k = 0; k < segs.size(); ++k) { diff --git a/src/llvm/lib/Target/W65816/W65816AsmPrinter.cpp b/src/llvm/lib/Target/W65816/W65816AsmPrinter.cpp index d06ca74..fc11f16 100644 --- a/src/llvm/lib/Target/W65816/W65816AsmPrinter.cpp +++ b/src/llvm/lib/Target/W65816/W65816AsmPrinter.cpp @@ -110,6 +110,22 @@ static constexpr unsigned kFrameRecordSize = 12; static constexpr unsigned kJslRtlBytes = 3; static constexpr const char *kFrameSection = ".debug_frame_w65816"; +// Phase B — opt-in far globals. A global declared via +// `__attribute__((section("X")))` where X contains ".far" (canonically +// .rodata.far / .data.far / .bss.far) routes through LDA_Long / STA_Long +// instead of LDA_Abs / STA_Abs at AsmPrinter pseudo expansion, so its +// bank byte is encoded explicitly and the linker patches the full +// 24-bit address via R_W65816_IMM24. This is the opt-in path for +// programs whose data overflows the small-data bank (DBR is pinned at +// the BSS-seg-0 bank by crt0; large rodata/BSS in other banks needs +// long-form addressing). +static bool isFarGlobalOperand(const llvm::MachineOperand &MO) { + if (!MO.isGlobal()) return false; + const llvm::GlobalValue *GV = MO.getGlobal(); + if (!GV->hasSection()) return false; + return GV->getSection().find(".far") != llvm::StringRef::npos; +} + namespace { class W65816AsmPrinter : public AsmPrinter { @@ -655,6 +671,16 @@ void W65816AsmPrinter::emitInstruction(const MachineInstr *MI) { // like *(uint16*)0x70). // $100..$FFFF -> Abs-form (DBR-relative). // non-zero bank -> Long-form (4-byte, bank-explicit). + // Far global -> Long-form (Phase B opt-in: a global declared + // via __attribute__((section("X"))) + // where X contains ".far" — usually + // .rodata.far, .data.far, or .bss.far + // — gets bank-explicit 24-bit access + // regardless of DBR. This is how a + // program with > 60 KB of data spreads + // it across multiple banks while keeping + // DBR pinned at the small-data bank for + // the rest of the program.) // The DP pickup is what makes the bank-0-anchored __indirTarget // slot at $00:00B8 (see runtime/src/libgcc.s) addressable correctly // from codegen-emitted indirect-call sequences when DBR != 0 (crt0 @@ -680,8 +706,12 @@ void W65816AsmPrinter::emitInstruction(const MachineInstr *MI) { return; } } + bool IsFarGlobal = AddrOp.isGlobal() && + AddrOp.getGlobal()->hasSection() && + AddrOp.getGlobal()->getSection().find(".far") != + StringRef::npos; EmitToStreamer(*OutStreamer, - MCInstBuilder(OpAbs) + MCInstBuilder(IsFarGlobal ? OpLong : OpAbs) .addOperand(lowerOperand(AddrOp, MCInstLowering))); return; } @@ -778,8 +808,12 @@ void W65816AsmPrinter::emitInstruction(const MachineInstr *MI) { case W65816::LDA8long: { // i8 absolute load — same M=8 wrap as LDA_Abs; LDA8long uses // LDA_Long (0xAF, bank-explicit) for const-int MMIO addresses. + // A global declared in a `.far*` section also routes through + // LDA_Long so its bank byte is encoded explicitly (Phase B). bool IsLong = MI->getOpcode() == W65816::LDA8long; - MCOperand Addr = lowerOperand(MI->getOperand(1), MCInstLowering); + const MachineOperand &AddrMO = MI->getOperand(1); + IsLong = IsLong || isFarGlobalOperand(AddrMO); + MCOperand Addr = lowerOperand(AddrMO, MCInstLowering); if (IsLong && Addr.isImm()) { // 16-bit pointer sign-extended into i32 imm — mask back to 16 bits // so the encoded bank byte is 0. See STA8long for the rationale. @@ -795,20 +829,27 @@ void W65816AsmPrinter::emitInstruction(const MachineInstr *MI) { case W65816::LDA8absX: { // i8 indexed-global load: SEP #0x20 ; LDA , X ; REP #0x20 // X holds the index (set up by CopyToReg before this MI). + // A `.far*` global routes through LDA_LongX (0xBF) for explicit + // 24-bit bank addressing (Phase B). + const MachineOperand &AddrMO = MI->getOperand(0); + bool IsFar = isFarGlobalOperand(AddrMO); emitSepM(); EmitToStreamer(*OutStreamer, - MCInstBuilder(W65816::LDA_AbsX) - .addOperand(lowerOperand(MI->getOperand(0), MCInstLowering))); + MCInstBuilder(IsFar ? W65816::LDA_LongX : W65816::LDA_AbsX) + .addOperand(lowerOperand(AddrMO, MCInstLowering))); emitRepM(); return; } case W65816::STA8absX: { // i8 indexed-global store: SEP #0x20 ; STA , X ; REP #0x20 - // A holds the value, X holds the index. + // A holds the value, X holds the index. `.far*` global routes + // through STA_LongX (0x9F) for explicit 24-bit bank addressing. + const MachineOperand &AddrMO = MI->getOperand(0); + bool IsFar = isFarGlobalOperand(AddrMO); emitSepM(); EmitToStreamer(*OutStreamer, - MCInstBuilder(W65816::STA_AbsX) - .addOperand(lowerOperand(MI->getOperand(0), MCInstLowering))); + MCInstBuilder(IsFar ? W65816::STA_LongX : W65816::STA_AbsX) + .addOperand(lowerOperand(AddrMO, MCInstLowering))); emitRepM(); return; } @@ -821,8 +862,10 @@ void W65816AsmPrinter::emitInstruction(const MachineInstr *MI) { // from STA8abs only in the underlying opcode (0x8F vs 0x8D): long // is bank-explicit, abs is DBR-relative. Long is required for // const-int MMIO addresses since the data bank is non-zero under - // GS/OS Loader. - bool IsLong = MI->getOpcode() == W65816::STA8long; + // GS/OS Loader. Far globals (`.far*` section) also route through + // STA_Long for explicit bank addressing (Phase B). + bool IsLong = MI->getOpcode() == W65816::STA8long || + isFarGlobalOperand(MI->getOperand(1)); bool UsesAcc8 = MI->getMF() ->getInfo() ->getUsesAcc8(); @@ -857,8 +900,12 @@ void W65816AsmPrinter::emitInstruction(const MachineInstr *MI) { case W65816::SBCabs: { bool IsSub = MI->getOpcode() == W65816::SBCabs; emitOp(IsSub ? W65816::SEC : W65816::CLC); + bool IsFar = isFarGlobalOperand(MI->getOperand(2)); + unsigned Mc = IsSub + ? (IsFar ? W65816::SBC_Long : W65816::SBC_Abs) + : (IsFar ? W65816::ADC_Long : W65816::ADC_Abs); EmitToStreamer(*OutStreamer, - MCInstBuilder(IsSub ? W65816::SBC_Abs : W65816::ADC_Abs) + MCInstBuilder(Mc) .addOperand(lowerOperand(MI->getOperand(2), MCInstLowering))); return; } @@ -866,8 +913,12 @@ void W65816AsmPrinter::emitInstruction(const MachineInstr *MI) { case W65816::SBCEabs: { // Chained variant — no CLC/SEC prefix. bool IsSub = MI->getOpcode() == W65816::SBCEabs; + bool IsFar = isFarGlobalOperand(MI->getOperand(2)); + unsigned Mc = IsSub + ? (IsFar ? W65816::SBC_Long : W65816::SBC_Abs) + : (IsFar ? W65816::ADC_Long : W65816::ADC_Abs); EmitToStreamer(*OutStreamer, - MCInstBuilder(IsSub ? W65816::SBC_Abs : W65816::ADC_Abs) + MCInstBuilder(Mc) .addOperand(lowerOperand(MI->getOperand(2), MCInstLowering))); return; } @@ -888,8 +939,9 @@ void W65816AsmPrinter::emitInstruction(const MachineInstr *MI) { return; } case W65816::CMPabs: { + bool IsFar = isFarGlobalOperand(MI->getOperand(1)); EmitToStreamer(*OutStreamer, - MCInstBuilder(W65816::CMP_Abs) + MCInstBuilder(IsFar ? W65816::CMP_Long : W65816::CMP_Abs) .addOperand(lowerOperand(MI->getOperand(1), MCInstLowering))); return; } @@ -912,11 +964,12 @@ void W65816AsmPrinter::emitInstruction(const MachineInstr *MI) { case W65816::ANDabs: case W65816::ORAabs: case W65816::EORabs: { + bool IsFar = isFarGlobalOperand(MI->getOperand(2)); unsigned mc = 0; switch (MI->getOpcode()) { - case W65816::ANDabs: mc = W65816::AND_Abs; break; - case W65816::ORAabs: mc = W65816::ORA_Abs; break; - case W65816::EORabs: mc = W65816::EOR_Abs; break; + case W65816::ANDabs: mc = IsFar ? W65816::AND_Long : W65816::AND_Abs; break; + case W65816::ORAabs: mc = IsFar ? W65816::ORA_Long : W65816::ORA_Abs; break; + case W65816::EORabs: mc = IsFar ? W65816::EOR_Long : W65816::EOR_Abs; break; } EmitToStreamer(*OutStreamer, MCInstBuilder(mc).addOperand(