// link816 — minimal flat-binary linker for W65816 ELF .o files. // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // // Reads one or more ELF32 object files (produced by llvm-mc / clang -c // with the W65816 backend), concatenates their .text* / .rodata* / // .data* sections at consecutive addresses starting from a given base, // builds a global symbol table, resolves the W65816 ELF relocations, // and writes a flat binary suitable for loading into a 65816 emulator // or further wrapping by omfEmit. // // Standalone — no LLVM dependency. Parses ELF32-LE structures // directly with the layout from /usr/include/elf.h. // // Supported relocation types (per W65816ELFObjectWriter): // 1 R_W65816_IMM8 — 1-byte absolute // 2 R_W65816_IMM16 — 2-byte LE absolute // 3 R_W65816_IMM24 — 3-byte LE absolute (JSL targets) // 4 R_W65816_PCREL8 — 1-byte signed PC-relative // 5 R_W65816_PCREL16 — 2-byte signed PC-relative // 6 R_W65816_BANK16 — 2-byte, high byte = bank of target, low = 0 // 7 R_W65816_DATA32 — 4-byte LE absolute (DWARF, .long) // 8 R_W65816_PCREL32 — 4-byte signed PC-relative (DWARF diffs) // // CLI mirrors the Python tool exactly: // link816 -o out.bin --text-base 0x8000 --bss-base 0x2000 a.o b.o ... // [--rodata-base ADDR] [--map FILE] #include #include #include #include #include #include #include #include #include #include #include #include namespace { // ---------------------------------------------------------------- ELF32 layout // We only need the LE host-side parsing path. Field names mirror // /usr/include/elf.h so a reader can cross-check against the spec. struct Elf32Ehdr { uint8_t e_ident[16]; uint16_t e_type; uint16_t e_machine; uint32_t e_version; uint32_t e_entry; uint32_t e_phoff; uint32_t e_shoff; uint32_t e_flags; uint16_t e_ehsize; uint16_t e_phentsize; uint16_t e_phnum; uint16_t e_shentsize; uint16_t e_shnum; uint16_t e_shstrndx; }; struct Elf32Shdr { uint32_t sh_name; uint32_t sh_type; uint32_t sh_flags; uint32_t sh_addr; uint32_t sh_offset; uint32_t sh_size; uint32_t sh_link; uint32_t sh_info; uint32_t sh_addralign; uint32_t sh_entsize; }; [[maybe_unused]] static constexpr uint32_t SHT_NULL = 0; [[maybe_unused]] static constexpr uint32_t SHT_PROGBITS = 1; static constexpr uint32_t SHT_SYMTAB = 2; static constexpr uint32_t SHT_STRTAB = 3; static constexpr uint32_t SHT_RELA = 4; [[maybe_unused]] static constexpr uint32_t SHT_NOBITS = 8; struct Elf32Sym { uint32_t st_name; uint32_t st_value; uint32_t st_size; uint8_t st_info; uint8_t st_other; uint16_t st_shndx; }; static constexpr uint16_t SHN_UNDEF = 0; static constexpr uint16_t SHN_ABS = 0xFFF1; static constexpr uint16_t SHN_COMMON = 0xFFF2; // W65816 ELF e_machine value. Vendor-private slot in the 0xFF00-0xFFFF // experimental range reserved by the ELF spec. Must match the value used // by W65816ELFObjectWriter and the EM_W65816 enumerator in // llvm/include/llvm/BinaryFormat/ELF.h. See docs/USAGE.md "ELF // e_machine value" section. static constexpr uint16_t EM_W65816 = 0xFF16; static constexpr uint16_t EM_NONE = 0; inline uint8_t ELF32_ST_TYPE(uint8_t i) { return i & 0x0F; } inline uint8_t ELF32_ST_BIND(uint8_t i) { return (i >> 4) & 0x0F; } static constexpr uint8_t STB_LOCAL = 0; [[maybe_unused]] static constexpr uint8_t STB_GLOBAL = 1; static constexpr uint8_t STB_WEAK = 2; [[maybe_unused]] static constexpr uint8_t STT_NOTYPE = 0; [[maybe_unused]] static constexpr uint8_t STT_OBJECT = 1; [[maybe_unused]] static constexpr uint8_t STT_FUNC = 2; static constexpr uint8_t STT_SECTION = 3; struct Elf32Rela { uint32_t r_offset; uint32_t r_info; int32_t r_addend; }; inline uint32_t ELF32_R_SYM (uint32_t i) { return i >> 8; } inline uint32_t ELF32_R_TYPE(uint32_t i) { return i & 0xFF; } // W65816 reloc type numbers — match W65816ELFObjectWriter. static constexpr uint8_t R_W65816_IMM8 = 1; static constexpr uint8_t R_W65816_IMM16 = 2; static constexpr uint8_t R_W65816_IMM24 = 3; static constexpr uint8_t R_W65816_PCREL8 = 4; static constexpr uint8_t R_W65816_PCREL16 = 5; // Bank byte of a 24-bit absolute address. Patches 2 bytes: // byte 0 = (target >> 16) & 0xFF, byte 1 = 0. Used for the high // half of `&symbol` 32-bit pointers so the bank tracks the actual // OMF Loader segment placement at runtime. cRELOC emission uses // ByteCnt=2 BitShift=16 so the Loader patches with // (segPlacedBase + offsetRef) >> 16. static constexpr uint8_t R_W65816_BANK16 = 6; // 4-byte LE absolute fixup. Generated for FK_Data_4 (non-PCRel) — // DWARF .debug_* section-relative addresses, .long directives. // The 65816 has a 24-bit address space, so the high byte is always // zero; we still write all 4 bytes so the slot width matches the // DWARF reader's expectation (every 4-byte address field decodes // as a clean 32-bit value, not 3 bytes + neighbour byte). static constexpr uint8_t R_W65816_DATA32 = 7; // 4-byte signed PC-relative fixup. Generated for FK_Data_4 (PCRel) — // section-relative DWARF diffs that the assembler can't resolve // in-section come through as PC-relative per // ELFObjectWriter::recordRelocation. static constexpr uint8_t R_W65816_PCREL32 = 8; // ---------------------------------------------------------------- IIgs memory map // Bank-0 hazard zones the placement logic must route around. Kept as // named constants to avoid sprinkling magic 0xC000 / 0xD000 across the // rodata/init/bss/heap placement code (previously: ~13 raw uses across // five distinct decisions). Update both halves together if the IIgs // memory map ever needs revisiting. // // $C000..$CFFF — IO and soft switches. Reads return hardware // register values, writes hit soft switches. Code, // data, and BSS placement all bump past this zone. // $D000..$DFFF — Language Card 1. Read-only ROM by default; crt0 // enables LC1 RAM via the $C083 read-twice trick so // rodata/BSS/heap placed here is writable. // $0001:0000 — Bank-0 ceiling; any range whose top exceeds this // must be split across banks (BSS handles up to 4 // consecutive banks; rodata/init are bank-0 only). static constexpr uint32_t kIoWindowStart = 0xC000; // $C000 static constexpr uint32_t kIoWindowEnd = 0xD000; // first usable byte past IO static constexpr uint32_t kBank0Ceiling = 0x10000; // first byte of bank 1 // Returns true iff `[start, start+size)` overlaps the IO window OR // starts inside it. Used by rodata / init_array / BSS placement. static inline bool overlapsIoWindow(uint32_t start, uint32_t size) { return start < kIoWindowEnd && (start + size) > kIoWindowStart; } // ---------------------------------------------------------------- Helpers [[noreturn]] static void die(const std::string &msg) { std::fprintf(stderr, "link816: %s\n", msg.c_str()); std::exit(1); } static std::vector readFile(const std::string &path) { std::ifstream f(path, std::ios::binary); if (!f) die("cannot open '" + path + "' for reading"); std::vector buf((std::istreambuf_iterator(f)), std::istreambuf_iterator()); 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"; if (name == ".data" || name.rfind(".data.", 0) == 0) return "rodata"; if (name == ".bss" || name.rfind(".bss.", 0) == 0) return "bss"; // .init_array entries are 16-bit function pointers; treat as // rodata so they end up in the read-only image and get a stable // address. The linker emits __init_array_start/_end so crt0 can // walk them. (.fini_array is not yet wired up; ELF input is // accepted but the sections are dropped — runtime has no // destructor-walk path today.) if (name == ".init_array" || name.rfind(".init_array.", 0) == 0) return "init_array"; // DWARF debug sections that are *targets* of intra-debug relocs // (e.g. .debug_info -> .debug_str via R_W65816_DATA32, or // .debug_str_offsets -> .debug_str via R_W65816_DATA32). Treat // them as a separate "debug" kind so resolveSym() can patch // intra-debug references with section-relative offsets (the // sidecar concatenates section data per object, preserving the // original object-local offset semantics). Without this, the // .debug_str_offsets entries stay zeroed and llvm-dwarfdump // can't resolve strx-form DW_AT_name attributes — every variable // name comes through as @strxN. if (name == ".debug_str" || name == ".debug_line_str" || name == ".debug_str_offsets" || name == ".debug_addr") return "debug"; return ""; } // ---------------------------------------------------------------- ELF parser struct Section { std::string name; uint32_t type; uint32_t size; uint32_t fileOffset; uint32_t link; uint32_t info; }; struct Symbol { std::string name; uint32_t value; // st_value uint16_t shndx; uint8_t type; // STT_* uint8_t bind; // STB_LOCAL / STB_GLOBAL / STB_WEAK }; struct Reloc { uint32_t offset; // within target section uint32_t symIdx; uint8_t type; int32_t addend; }; struct InputObject { std::string path; std::vector raw; std::vector
sections; std::vector symbols; // relocs indexed by target section id std::map> relocs; void parse() { if (raw.size() < sizeof(Elf32Ehdr)) die("'" + path + "': file too small to be ELF"); if (raw[0] != 0x7f || raw[1] != 'E' || raw[2] != 'L' || raw[3] != 'F') die("'" + path + "': not an ELF file"); if (raw[4] != 1) // ELFCLASS32 die("'" + path + "': not 32-bit ELF"); if (raw[5] != 1) // ELFDATA2LSB die("'" + path + "': not little-endian ELF"); Elf32Ehdr hdr; std::memcpy(&hdr, raw.data(), sizeof(hdr)); // e_machine: accept EM_W65816 (canonical, set by our object writer) // and EM_NONE (pre-Phase-1.13 objects, in case anyone still has stale // .o files in a build tree). Reject anything else — a host-arch .o // accidentally fed in here would silently link otherwise. if (hdr.e_machine != EM_W65816 && hdr.e_machine != EM_NONE) { char msg[256]; std::snprintf(msg, sizeof(msg), "'%s': wrong e_machine (got 0x%04X, expected EM_W65816=0xFF16)", path.c_str(), hdr.e_machine); die(msg); } if (hdr.e_shoff == 0 || hdr.e_shnum == 0) die("'" + path + "': no section table"); if (hdr.e_shentsize != sizeof(Elf32Shdr)) die("'" + path + "': unexpected section header size"); // Section header string table — used to look up section names. Elf32Shdr shstrhdr; std::memcpy(&shstrhdr, raw.data() + hdr.e_shoff + hdr.e_shstrndx * sizeof(Elf32Shdr), sizeof(shstrhdr)); const char *shstrtab = reinterpret_cast( raw.data() + shstrhdr.sh_offset); sections.resize(hdr.e_shnum); std::vector shdrs(hdr.e_shnum); for (size_t i = 0; i < hdr.e_shnum; ++i) { std::memcpy(&shdrs[i], raw.data() + hdr.e_shoff + i * sizeof(Elf32Shdr), sizeof(Elf32Shdr)); sections[i].name = std::string(shstrtab + shdrs[i].sh_name); sections[i].type = shdrs[i].sh_type; sections[i].size = shdrs[i].sh_size; sections[i].fileOffset = shdrs[i].sh_offset; sections[i].link = shdrs[i].sh_link; sections[i].info = shdrs[i].sh_info; } // Find the symbol table and its string table. size_t symtabIdx = (size_t)-1, symstrtabIdx = (size_t)-1; for (size_t i = 0; i < sections.size(); ++i) { if (sections[i].type == SHT_SYMTAB) { symtabIdx = i; symstrtabIdx = sections[i].link; break; } } if (symtabIdx == (size_t)-1) { // Object with no symbols is unusual but legal — treat as empty. return; } const char *symstrtab = reinterpret_cast( raw.data() + sections[symstrtabIdx].fileOffset); size_t numSyms = sections[symtabIdx].size / sizeof(Elf32Sym); symbols.resize(numSyms); for (size_t i = 0; i < numSyms; ++i) { Elf32Sym sym; std::memcpy(&sym, raw.data() + sections[symtabIdx].fileOffset + i * sizeof(Elf32Sym), sizeof(Elf32Sym)); symbols[i].name = std::string(symstrtab + sym.st_name); symbols[i].value = sym.st_value; symbols[i].shndx = sym.st_shndx; symbols[i].type = ELF32_ST_TYPE(sym.st_info); symbols[i].bind = ELF32_ST_BIND(sym.st_info); } // Walk RELA sections; index by their target section (sh_info). for (size_t i = 0; i < sections.size(); ++i) { if (sections[i].type != SHT_RELA) continue; uint32_t targetSec = sections[i].info; size_t numRels = sections[i].size / sizeof(Elf32Rela); std::vector &out = relocs[targetSec]; out.reserve(numRels); for (size_t j = 0; j < numRels; ++j) { Elf32Rela r; std::memcpy(&r, raw.data() + sections[i].fileOffset + j * sizeof(Elf32Rela), sizeof(Elf32Rela)); Reloc R; R.offset = r.r_offset; R.symIdx = ELF32_R_SYM(r.r_info); R.type = static_cast(ELF32_R_TYPE(r.r_info)); R.addend = r.r_addend; out.push_back(R); } } } const uint8_t *sectionData(uint32_t idx) const { return raw.data() + sections[idx].fileOffset; } std::vector sectionsByKind(const std::string &kind) const { std::vector out; for (size_t i = 0; i < sections.size(); ++i) { if (sections[i].size == 0) continue; if (sectionKind(sections[i].name) == kind) out.push_back(static_cast(i)); } return out; } }; // ---------------------------------------------------------------- Linker // Multi-segment text layout. Segment 1 lives in bank 0 alongside // rodata/bss/init_array (the existing single-segment layout). When // --segment-cap is set and total text exceeds it, additional code // segments get bank-aligned bases starting at --segment-bank-base // (default 0x040000 = bank 4 — chosen to leave bank 0 + LC RAM for // segment 1, banks 1-3 free for data / markers / future use). struct TextSeg { uint32_t segNum = 1; // 1-based; 1 is the bank-0 segment uint32_t base = 0; // 24-bit load address uint32_t size = 0; // bytes occupied std::vector body; // patched bytes ready to write }; struct Layout { uint32_t textBase, textSize; // segment 1's text (bank 0) uint32_t rodataBase, rodataSize; uint32_t bssBase, bssSize; uint32_t initBase, initSize; // segments[0] = segment 1 (bank 0); segments[1..] = bank-N+ overflow. // Always at least one entry. std::vector segments; }; // One IMM-N relocation site (N=2 or 3), recorded for OMF cRELOC // emission. The Loader rewrites the N bytes at `patchOff` to be // (segPlacedBase + offsetRef) when the segment is placed at runtime // — this fixes both 24-bit JSL/JML/STAlong AND 16-bit absolute data // references when the segment doesn't land at link-time-base. struct Imm24Site { uint32_t patchOff; // offset within text image (== patchAddr - textBase) uint32_t offsetRef; // offset within text image of target symbol uint8_t byteCnt; // 2 = IMM16 or BANK16, 3 = IMM24 uint8_t bitShift; // 0 for IMM16/IMM24, 16 for BANK16 }; static std::vector gImm24Sites; static uint32_t gTextBaseForSites = 0; static bool gRecordSites = false; static unsigned long gBank0DataSites = 0; // --report-self-mod: audit diagnostic. Reports every intra-.text-section // IMM16 reference made with a STORE opcode (absolute, DBR-relative) into the // section's OWN code -- i.e. self-modifying code. Such a store only reaches // the real instruction when the code's bank equals DBR; if the linker places // the section in a non-DBR bank (seg2) it writes to the wrong bank and // corrupts the patch. The fix is to make the self-mod bank-explicit (DBR=PBR // around it). PC-relative branches, jsr/jmp (PBR), and immediate (#) operands // are bank-safe and not reported. static bool gReportSelfMod = 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 // ---- Cross-segment function-pointer TRAMPOLINE table (GS/OS jump-table // model, all-resident form). clang forms &func as a 16-bit offset + the bank // from $00BE (the LOAD/seg1 bank), so &func of a function the linker placed // in seg2+ (a different bank) gets the WRONG (seg1) bank and crashes when the // pointer is called. Fix: for each cross-seg-function IMM16 address-take, // emit a `JML func` trampoline in SEG1 (offset + $BE seg1 bank are then // correct) and redirect the IMM16 to it; the JML's 24-bit target is cINTERSEG // -relocated so the Loader patches the real seg2 bank. Calling through the // pointer reaches the seg1 trampoline and tail-jumps (JML) into the function. // (The matching call-site half -- libgcc __jsl_indir widened to JML[$00B8], // 24-bit -- lets the dispatch honor the bank.) Keyed by the target's // (segNum, offsetWithinSeg) so one trampoline serves every reference. struct TrampKey { uint32_t seg; uint32_t off; bool operator<(const TrampKey &o) const { return seg != o.seg ? seg < o.seg : off < o.off; } }; static std::map gTrampMap; // key -> trampoline index static std::vector gTrampList; // index -> key (emit order) static uint32_t gTrampBase = 0; // byte offset of the table within seg1 static constexpr uint32_t kTrampBytes = 4; // JML abs-long: 0x5C + 24-bit // Record an intra-segment patch site for cRELOC emission. A target // below the text base is never intra-segment (it is an undefined-weak // resolving to 0, or an absolute address) and is skipped — see the // commentary at the R_W65816_IMM16 callsite for why this matters. static void recordCRelocSite(uint32_t patchAddr, uint32_t target, uint8_t byteCnt, uint8_t bitShift) { if (!gRecordSites) return; uint32_t targetBank = target & 0xFF0000; uint32_t baseBank = gTextBaseForSites & 0xFF0000; if (targetBank != baseBank) return; if (target < gTextBaseForSites) return; Imm24Site s; s.patchOff = patchAddr - gTextBaseForSites; s.offsetRef = target - gTextBaseForSites; s.byteCnt = byteCnt; s.bitShift = bitShift; 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 // types (the caller should reject the reloc). static uint32_t relocWidth(uint8_t rtype) { switch (rtype) { case R_W65816_IMM8: case R_W65816_PCREL8: return 1; case R_W65816_IMM16: case R_W65816_PCREL16: case R_W65816_BANK16: return 2; case R_W65816_IMM24: return 3; case R_W65816_DATA32: case R_W65816_PCREL32: return 4; default: return 0; } } unsigned long gDbgBareCalls = 0; unsigned long gDbgTextCalls = 0; static void applyReloc(std::vector &buf, uint32_t off, uint32_t patchAddr, uint32_t target, uint8_t rtype, const std::string &symName) { extern unsigned long gDbgBareCalls; gDbgBareCalls++; if (symName.find("gRnInPack") != std::string::npos) { std::fprintf(stderr, "TRACE-BARE %s rtype=%u target=0x%X off=0x%X\n", symName.c_str(), rtype, target, off); } int64_t pcrelDisp; switch (rtype) { case R_W65816_IMM8: if (target > 0xFF) die("R_W65816_IMM8 to '" + symName + "' = 0x" + std::to_string(target) + " out of range"); buf[off] = static_cast(target & 0xFF); break; case R_W65816_IMM16: // Keep only the low 16 bits. In single-bank programs this // is a tautology (target IS 16-bit); in multi-segment // programs the target may live in a different bank, but // IMM16 absolute uses DBR for the bank at runtime, so // patching just the low 16 bits is correct as long as the // caller's DBR points at the target's bank. buf[off] = static_cast(target & 0xFF); buf[off + 1] = static_cast((target >> 8) & 0xFF); // Record for cRELOC emission so the Loader patches this // 2-byte operand to (segPlacedBase + offsetRef) at load // time. Without this, `lda absConst` reads from the wrong // address when the segment doesn't land at link-time-base // (e.g., link-time-base=0x1000 but Loader places at bank:0). // A target below the text base is never an intra-segment // relocatable site: it is an undefined-weak symbol (resolveSym // resolves those to 0) or an absolute address. Recording a // cRELOC for it would (a) underflow offsetRef = target - textBase // (omfEmit rejects it as out-of-range) and (b) make the Loader // rewrite a genuine null to segPlacedBase, breaking the // `if (weakFn) weakFn()` null test that the null is meant to fail. // recordCRelocSite handles the gate; byteCnt=2 distinguishes // from IMM24 (3) so omfEmit emits cRELOC ByteCnt=2 here. // Optimization: when text-base's low 16 bits are zero, the // link-time baked operand (target & 0xFFFF) is already equal // to (segPlacedBase + offsetRef) & 0xFFFF — because the // Loader places KIND=0x1000 segments at bank-aligned bases // (segPlacedBase.lo16 = 0), and we baked target = textBase + // offsetRef, so the patch is a no-op. Skipping it eliminates // 50–90% of cRELOC entries in typical builds (most data refs // are IMM16); the GS/OS Loader walks each cRELOC site in a // linear loop, so the per-boot reloc-application time drops // proportionally. Programs that link with a non-bank-aligned // text-base (e.g. bare-metal at $1000) still get the cRELOC. if ((gTextBaseForSites & 0xFFFFu) != 0) recordCRelocSite(patchAddr, target, /*byteCnt=*/2, /*bitShift=*/0); break; case R_W65816_BANK16: // 2-byte patch: byte 0 = bank of target, byte 1 = 0 (pad). // The Loader's cRELOC pass overwrites these bytes with // (segPlacedBase + offsetRef) >> 16 at load time, so at runtime // the value reflects the actually-placed bank. buf[off] = static_cast((target >> 16) & 0xFF); buf[off + 1] = 0; // bitShift=16: cRELOC Loader patches the bank byte from // (segPlacedBase + offsetRef) >> 16 at load time. recordCRelocSite(patchAddr, target, /*byteCnt=*/2, /*bitShift=*/16); break; case R_W65816_IMM24: if (target > 0xFFFFFF) die("R_W65816_IMM24 to '" + symName + "' = 0x" + std::to_string(target) + " out of range"); buf[off] = static_cast(target & 0xFF); buf[off + 1] = static_cast((target >> 8) & 0xFF); buf[off + 2] = static_cast((target >> 16) & 0xFF); // Only intra-segment refs need cRELOC; cross-bank refs (to // GS/OS dispatcher etc.) target absolute fixed addresses // and shouldn't be relocated by the Loader. recordCRelocSite // applies the same gates as R_W65816_IMM16. recordCRelocSite(patchAddr, target, /*byteCnt=*/3, /*bitShift=*/0); break; case R_W65816_PCREL8: pcrelDisp = static_cast(target) - (static_cast(patchAddr) + 1); if (pcrelDisp < -128 || pcrelDisp > 127) { char msg[256]; std::snprintf(msg, sizeof(msg), "R_W65816_PCREL8 to '%s' out of branch range (%lld bytes)", symName.c_str(), (long long)pcrelDisp); die(msg); } buf[off] = static_cast(pcrelDisp & 0xFF); break; case R_W65816_PCREL16: pcrelDisp = static_cast(target) - (static_cast(patchAddr) + 2); if (pcrelDisp < -32768 || pcrelDisp > 32767) die("R_W65816_PCREL16 to '" + symName + "' out of BRL range"); buf[off] = static_cast(pcrelDisp & 0xFF); buf[off + 1] = static_cast((pcrelDisp >> 8) & 0xFF); break; case R_W65816_DATA32: // 4-byte LE absolute. Used in DWARF .debug_* sections // (sectionBase + addend) and user `.long` directives. The // 65816 has only a 24-bit address space, so the high byte // is always zero — but we MUST write all 4 bytes because // DWARF readers expect a clean 32-bit slot. Writing only 3 // bytes corrupts the next field (often a length / opcode in // .debug_line, leading to unit_length = 0 footgun this // patch series exists to fix). buf[off] = static_cast(target & 0xFF); buf[off + 1] = static_cast((target >> 8) & 0xFF); buf[off + 2] = static_cast((target >> 16) & 0xFF); buf[off + 3] = 0; // Record a cRELOC site for intra-segment DATA32 references so // the OMF Loader patches the 24-bit address (low/mid/bank) // when the segment is placed at a non-zero bank. This is the // C-ABI-critical path: a `void *` field in a static parm // block (e.g. `__GsosOpenParm.pathname`) gets emitted by the // compiler as `.long path`, which we lower as DATA32. Without // a cRELOC the slot stays at link-time bank=0, GS/OS reads // the parm block's pathname pointer as $00:offset, and Open // fails with $40 (invalidAccess on a garbage path). The // DWARF .debug_* path is excluded by the target-in-segment // check (debug sections live in the sidecar, addresses below // textBase) -- same guard as IMM16/BANK16/IMM24. ByteCnt=3 // patches the low 3 bytes of the 4-byte slot at load time, // leaving the high (pad) byte at 0 (writes the resolved // 24-bit value bank:offset with bitShift=0 == no shift). recordCRelocSite(patchAddr, target, /*byteCnt=*/3, /*bitShift=*/0); break; case R_W65816_PCREL32: // 4-byte signed PC-relative. PCREL displacements have the // PC pointing past the slot — the convention used by every // other PCREL reloc in this file (PCREL8 adds 1, PCREL16 // adds 2), so PCREL32 adds 4. pcrelDisp = static_cast(target) - (static_cast(patchAddr) + 4); // No range check: 32-bit signed displacement covers the // full address space. In practice this fires for DWARF // intra-section diffs where target and patchAddr live in // the same section, so pcrelDisp is small. buf[off] = static_cast(pcrelDisp & 0xFF); buf[off + 1] = static_cast((pcrelDisp >> 8) & 0xFF); buf[off + 2] = static_cast((pcrelDisp >> 16) & 0xFF); buf[off + 3] = static_cast((pcrelDisp >> 24) & 0xFF); break; default: { char msg[128]; std::snprintf(msg, sizeof(msg), "unhandled relocation type %u to '%s'", rtype, symName.c_str()); die(msg); } } } // 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, 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; } } uint32_t targetSeg = findSegContaining(target, segments); // Cross-segment FUNCTION POINTER (IMM16 &func to a function in seg2+): // clang forms the pointer as func's 16-bit offset + the $00BE seg1 bank, // which is wrong for a function in another bank. Redirect the IMM16 to // the seg1 JML trampoline reserved for this target (pre-pass) so the // pointer (seg1 offset + seg1 bank) is valid and tail-jumps into the real // function. Runs BEFORE the cross-seg/intra split because the common // crash is an INTRA-seg2 &func (targetSeg == patchSeg); targetSeg>1 (a // non-seg1 bank) is the real key. No cRELOC: the baked value is a seg1 // offset and seg1 (KIND=0x1000) always bank-aligns, so the low16 is // already the runtime offset (matches the --text-base 0 IMM16 contract). // Any multi-segment layout needs this, not just the GS/OS ExpressLoad // one: a bare-metal --segment-cap link (fixed bank bases, no Loader) // forms &func the same way, and without the trampoline a pointer to a // seg2+ function is called in bank 0 (Lua's allocator, the first thing // lua_newstate calls, ran into vfprintf and the machine went wild). if (rtype == R_W65816_IMM16 && targetSeg > 1) { uint32_t targetOff = target - segments[targetSeg - 1].base; auto tIt = gTrampMap.find(TrampKey{targetSeg, targetOff}); if (tIt != gTrampMap.end()) { uint32_t trampAddr = segments[0].base + gTrampBase + kTrampBytes * tIt->second; applyReloc(buf, off, patchAddr, trampAddr, rtype, symName); return; } } 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]; gDbgTextCalls++; // 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) { // BANK-0 DATA/BSS target (inside SEG1's payload) referenced from an overflow segment: the // GS/OS Loader honors at most PAGE alignment (it ignores OMF ALIGN=$10000), so seg1 loads // at an arbitrary in-bank offset and every baked data operand from another segment misses // by that offset (measured live on the RetroNet IIgs client: functions packed into seg1 // worked, everything else read/wrote unowned memory - MAME memory-tap proven). Emit // cINTERSEG records against seg 1 so the Loader writes the true placed address: 3 bytes // for IMM24/DATA32, 2 for IMM16 (bank comes from DBR - BANKSIZE=$10000 keeps seg1 within // one bank), 2+shift16 for BANK16. if (gRecordSites && patchSeg >= 1 && !segments.empty() && (rtype == R_W65816_IMM24 || rtype == R_W65816_DATA32 || rtype == R_W65816_IMM16 || rtype == R_W65816_BANK16) && (target & 0xFF0000u) == 0u && target != 0u) { uint8_t cnt = (rtype == R_W65816_IMM24 || rtype == R_W65816_DATA32) ? 3 : 2; for (uint8_t i = 0; i < cnt; ++i) buf[off + i] = 0; // The Loader adds seg 1's load address to an OFFSET within seg 1, // so subtract seg 1's link base. (Passing the link address only // worked at --text-base 0; at 0x1000 every such data reference // landed 0x1000 past its variable -- smoke Phase C.2 read an // initialised global as 0.) InterImm24Site s; s.patchSeg = static_cast(patchSeg); s.patchOff = static_cast(patchAddr - pseg.base); s.targetSeg = 1; s.targetOff = static_cast(target - segments[0].base); s.byteCnt = cnt; s.bitShift = (rtype == R_W65816_BANK16) ? 16 : 0; gInterImm24Sites.push_back(s); gBank0DataSites++; return; } 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; uint32_t rodataBase = 0; uint32_t bssBase = 0x2000; bool gcSections = true; // Multi-segment support. segmentCap == 0 means "no cap" — produce // a single-segment image (existing behaviour). Non-zero caps the // bytes per text segment; overflow text sections get bank-aligned // bases starting at segmentBankBase. uint32_t segmentCap = 0; // Cap for OVERFLOW segments (seg 2+). --segment-cap exists to bound // how much text stays in the ENTRY bank, which also has to hold // rodata, bss and the heap under the $C000 IO window. Overflow banks // carry text ONLY and have the full 64K free, so applying the entry // bank's tiny budget to them wastes ~50K per bank. 0 = same as // segmentCap (the historical behaviour). uint32_t overflowCap = 0; // Set by --full-bank: this image owns a whole 64K bank, so there is // no $C000-$CFFF IO window and no $D000+ language card to dodge, and // text / rodata / .init_array / BSS may use the bank right up to the // ceiling. // // That is true of a GS/OS OMF image: the Loader asks the Memory // Manager for each bank-aligned segment and gets whatever is free // (measured under MAME: joeylib's examples run in banks $08-$0B, and // bank $00 appears only in ROM and GS/OS glue). Those banks are plain // RAM throughout. // // It is NOT true of an image raw-loaded into bank 0, which is what // scripts/runInMame.sh does (loads at $00:1000 and jumps) for THIS // repo's smoke, bench and probe harnesses (clang-smoke.sh, // benchCyclesCalypsi.sh, tests/ubsan, tests/coremark, demos/build.sh -- // none of them pass --full-bank). joeylib's bench-iigs.sh is NOT one of // them: it boots GS/OS from joey.2mg, so it is an OMF consumer. // // For the raw-load harnesses the hazards are real and measured: smoke // #86 had string literals read back as IO register values, and smoke // #128 had BSS clearing hit soft switches and crash MAME. // // DEFAULT OFF, i.e. assume bank-0 hazards. The conservative layout is // merely smaller, while guessing "full bank" for an image that really // does land in bank 0 corrupts it -- so every raw-load consumer keeps // working untouched and only the OMF driver opts in. bool fullBank = false; 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. // For GS/OS apps: filetype=$B3 (S16), aux=0. // For GNO shell commands: filetype=$B5 (EXE), aux=0 (or $DC00 for // non-compliant programs that bypass GNO's keyboard buffer). // -1 sentinel = "not set" (caller hasn't asked for a sidecar). int32_t fileType = -1; int32_t auxType = -1; // When true, writeMap() also dumps STB_LOCAL symbols (function- // internal labels like __udivmod_core, file-static C objects, etc). // Required by the function-attribution profiler so PC samples that // fall inside libgcc helpers / file-static functions get attributed // to a meaningful name instead of '?'. OFF by default to keep // smoke greps that depend on global-only map output stable. bool mapLocals = false; // Per-section identity: (object index, section index within obj). using SecID = std::pair; std::set liveSecs; std::map symToSection; // Build the "global symbol name -> (objIdx, secIdx) where defined" // map. Honors weak vs strong: strong def overrides weak; first // weak-only def wins. Used by computeLiveSet() to follow cross- // object reloc references back to their defining section. void buildSymToSection() { std::map strongSeen; for (size_t fi = 0; fi < objs.size(); ++fi) { const auto &obj = *objs[fi]; for (const Symbol &sym : obj.symbols) { if (sym.name.empty()) continue; if (sym.bind == STB_LOCAL) continue; if (sym.shndx == SHN_UNDEF || sym.shndx == SHN_ABS || sym.shndx == SHN_COMMON || sym.shndx >= obj.sections.size()) continue; bool thisStrong = (sym.bind != STB_WEAK); auto sit = strongSeen.find(sym.name); if (sit == strongSeen.end()) { symToSection[sym.name] = {fi, sym.shndx}; strongSeen[sym.name] = thisStrong; } else if (thisStrong && !sit->second) { symToSection[sym.name] = {fi, sym.shndx}; sit->second = true; } } } } // Compute the live-section set via BFS from roots (entry point, // init_array sections — crt0 walks them at runtime). Without // gc-sections, every section is implicitly live. void computeLiveSet() { if (!gcSections) return; buildSymToSection(); std::vector work; auto markLive = [&](SecID s) { if (liveSecs.insert(s).second) work.push_back(s); }; // Roots: entry symbols. __start is the canonical crt0 entry; // also keep main (crt0 calls it) and __indirTarget (used by // __jsl_indir). Plus any defined symbol whose name starts // with __ (linker-defined globals like __heap_start are also // synthesized but their section refs follow naturally). for (const char *root : {"__start", "_start", "main", "__indirTarget", "__jsl_indir"}) { auto it = symToSection.find(root); if (it != symToSection.end()) markLive(it->second); } // crt0's init-loop walks .init_array via the linker-defined // boundary symbols __init_array_start/_end. All init_array // sections must therefore be considered live. Same for // .fini_array if any object provides it. for (size_t fi = 0; fi < objs.size(); ++fi) { for (uint32_t idx : objs[fi]->sectionsByKind("init_array")) markLive({fi, idx}); } // BFS: each live section's relocs reference symbols whose // defining sections are in turn live. Local refs via section // symbols (STT_SECTION) resolve within the same object. for (size_t i = 0; i < work.size(); ++i) { SecID cur = work[i]; const auto &obj = *objs[cur.first]; auto relIt = obj.relocs.find(cur.second); if (relIt == obj.relocs.end()) continue; for (const Reloc &r : relIt->second) { if (r.symIdx >= obj.symbols.size()) continue; const Symbol &sym = obj.symbols[r.symIdx]; if (sym.shndx != SHN_UNDEF && sym.shndx != SHN_ABS && sym.shndx != SHN_COMMON && sym.shndx < obj.sections.size()) { // Local def (incl. STT_SECTION refs). markLive({cur.first, sym.shndx}); continue; } // External — look up the global definition. auto sit = symToSection.find(sym.name); if (sit != symToSection.end()) markLive(sit->second); // Else: undefined external; resolveSym() will die later // (or the user explicitly declared the ref weak). } } } bool isLive(size_t fi, uint32_t idx) const { if (!gcSections) return true; return liveSecs.count({fi, idx}) > 0; } // Per-object, per-section: in-merged-text/rodata/bss offset. // For text: textWithin gives the offset within the *segment* the // section is placed in; textSegOf names which segment (1-based). // Single-segment builds put everything in segment 1; multi-segment // builds may scatter sections across segments. struct ObjOffsets { uint32_t textBaseInMerged = 0; uint32_t rodataBaseInMerged = 0; uint32_t bssBaseInMerged = 0; uint32_t initBaseInMerged = 0; std::map textWithin; // offset within its segment std::map textSegOf; // section idx -> segment num (1-based) std::map rodataWithin; std::map bssWithin; std::map initWithin; }; std::vector objOff; std::map globalSyms; // Local symbol map (STB_LOCAL). Populated alongside globalSyms but // kept separate so resolution never accidentally picks a name-collided // local from another TU. Emitted by writeMap when --map-locals is set. // Key format: "name@objBasename" so two TUs each with a file-static // helper of the same name don't collide on insertion. std::map localSyms; // Load one ELF object or, when `path` is a Unix `ar` archive, every object // member it holds. Archive support lets JoeyLib examples link against a // real libjoey.a exactly like the other ports; --gc-sections then drops // whatever the image doesn't reference, matching classic member selection. void addObject(const std::string &path) { std::vector data = readFile(path); static const unsigned char AR_MAGIC[8] = {'!', '<', 'a', 'r', 'c', 'h', '>', '\n'}; if (data.size() >= 8 && std::memcmp(data.data(), AR_MAGIC, 8) == 0) { addArchive(path, data); return; } auto o = std::make_unique(); o->path = path; o->raw = std::move(data); o->parse(); objs.push_back(std::move(o)); } // Split a GNU-format `ar` archive into its object members and load each as // its own InputObject (path "archive.a(member)", so the file-static // local-symbol key name@basename stays unique per member). Skips the // symbol index ("/") and long-name string table ("//"). void addArchive(const std::string &path, const std::vector &data) { size_t pos = 8; // past "!\n" std::string longNames; // "//" GNU long-name table size_t members = 0; while (pos + 60 <= data.size()) { const char *hdr = reinterpret_cast(&data[pos]); if (hdr[58] != '`' || hdr[59] != '\n') die("'" + path + "': corrupt archive member header"); char sizeField[11]; std::memcpy(sizeField, hdr + 48, 10); sizeField[10] = '\0'; long msize = std::strtol(sizeField, nullptr, 10); if (msize < 0) die("'" + path + "': bad archive member size"); size_t dataOff = pos + 60; if (dataOff + static_cast(msize) > data.size()) die("'" + path + "': archive member overruns file"); char nameField[17]; std::memcpy(nameField, hdr, 16); nameField[16] = '\0'; std::string name(nameField); while (!name.empty() && name.back() == ' ') name.pop_back(); bool isObject = true; if (name == "/" || name == "/SYM64/" || name == "__.SYMDEF" || name == "__.SYMDEF SORTED") { isObject = false; // archive symbol index } else if (name == "//") { longNames.assign(reinterpret_cast(&data[dataOff]), static_cast(msize)); isObject = false; // GNU long-name string table } else if (name.size() >= 2 && name[0] == '/' && std::isdigit((unsigned char)name[1])) { size_t off = static_cast( std::strtoul(name.c_str() + 1, nullptr, 10)); name.clear(); // "/" -> resolve in "//" for (size_t i = off; i < longNames.size() && longNames[i] != '\n' && longNames[i] != '/'; ++i) name.push_back(longNames[i]); } else if (!name.empty() && name.back() == '/') { name.pop_back(); // GNU short name "foo.o/" } if (isObject) { auto o = std::make_unique(); o->path = path + "(" + name + ")"; o->raw.assign(data.begin() + dataOff, data.begin() + dataOff + static_cast(msize)); o->parse(); objs.push_back(std::move(o)); ++members; } pos = dataOff + static_cast(msize); if (pos & 1) ++pos; // members are 2-byte aligned } if (members == 0) die("'" + path + "': archive holds no object members"); } // Resolve a reloc to (target, name) using the symbol table and the // per-object section base map. Requires link() to have populated // objOff/globalSyms/lastLayout first. Returns false when the // 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, 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]; if (sym.type == STT_SECTION) { if (sym.shndx >= obj.sections.size()) die(obj.path + ": section symbol shndx out of range"); const auto &refSec = obj.sections[sym.shndx]; std::string kind = sectionKind(refSec.name); uint32_t base = 0; if (kind == "text") { auto wIt = oo.textWithin.find(sym.shndx); auto sIt = oo.textSegOf.find(sym.shndx); uint32_t segNum = (sIt == oo.textSegOf.end()) ? 1 : sIt->second; uint32_t segBase = (segNum >= 1 && segNum <= lastLayout.segments.size()) ? lastLayout.segments[segNum - 1].base : lastLayout.textBase; base = segBase + (wIt == oo.textWithin.end() ? 0 : wIt->second); } else if (kind == "rodata") { auto wIt = oo.rodataWithin.find(sym.shndx); base = lastLayout.rodataBase + oo.rodataBaseInMerged + (wIt == oo.rodataWithin.end() ? 0 : wIt->second); } else if (kind == "bss") { auto wIt = oo.bssWithin.find(sym.shndx); base = lastLayout.bssBase + oo.bssBaseInMerged + (wIt == oo.bssWithin.end() ? 0 : wIt->second); } else if (kind == "init_array") { auto wIt = oo.initWithin.find(sym.shndx); base = lastLayout.initBase + oo.initBaseInMerged + (wIt == oo.initWithin.end() ? 0 : wIt->second); } else if (kind == "debug") { // Intra-debug reference (e.g., .debug_info entry that // refers to a string at offset N in .debug_str). The // sidecar emits each object's debug sections back-to- // back without recompacting offsets, so a section- // relative target IS the right value to patch — base // is 0 and the addend carries the in-section offset. base = 0; } else { resolvedName = refSec.name; return false; } target = base + r.addend; resolvedName = refSec.name; return true; } auto sIt = globalSyms.find(sym.name); if (sIt == globalSyms.end()) { // Undefined symbol. If the reference itself is weak, ELF // semantics say "leave the address as 0" — do that and // return resolved-true so the caller doesn't error out. // This lets libc reference optional helpers (e.g. the // GS/OS file backend in iigsGsos.s) without forcing every // caller to link iigsGsos.o. Strong unresolved → caller // dies. resolvedName = sym.name; if (sym.bind == STB_WEAK) { target = 0 + r.addend; if (weakNull) *weakNull = true; return true; } return false; } target = sIt->second + r.addend; resolvedName = sym.name; return true; } Layout link(std::vector &outImage) { // 1. Layout: each obj's sections at running offsets. // Text is segment-aware: when --segment-cap is set and total // text would exceed it, sections spill into segments 2, 3, ... // each based at successive bank boundaries starting from // segmentBankBase. Other sections (rodata/bss/init_array) // stay in segment 1's bank for v1 — multi-bank data refs // would need IMM16 promotion to long which we don't do yet. objOff.resize(objs.size()); uint32_t curText = 0, curRodata = 0, curBss = 0, curInit = 0; 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; 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; // Entry bank shares its 64K with rodata/bss/heap; // overflow banks hold text only. uint32_t capHere = (curSeg == 1 || overflowCap == 0) ? segmentCap : overflowCap; if (!capHere) break; if (segSizes[curSeg - 1] + sz <= capHere) break; curSeg++; } } else if (curSeg > segSizes.size()) { segSizes.push_back(0); } 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; oo.rodataWithin[idx] = curRodata - oo.rodataBaseInMerged; curRodata += objs[fi]->sections[idx].size; } oo.bssBaseInMerged = curBss; for (uint32_t idx : objs[fi]->sectionsByKind("bss")) { if (!isLive(fi, idx)) continue; oo.bssWithin[idx] = curBss - oo.bssBaseInMerged; curBss += objs[fi]->sections[idx].size; } oo.initBaseInMerged = curInit; for (uint32_t idx : objs[fi]->sectionsByKind("init_array")) { if (!isLive(fi, idx)) continue; oo.initWithin[idx] = curInit - oo.initBaseInMerged; curInit += objs[fi]->sections[idx].size; } } // ---- TRAMPOLINE pre-pass (multi-segment text only). Find every IMM16 // address-take of a function the layout placed in a text seg N>1 (a // different bank than seg1, whose bank is what $00BE/&func will use) // and reserve one `JML func` trampoline in seg1 per distinct target. // Done HERE -- after section->seg packing, before the segment bases -- // so growing segSizes[0] shifts rodata/bss/globalSyms automatically // (no second pass). A name->(seg,off) map mirrors the globalSyms // strong/weak resolution so these keys match findSegContaining()'s in // applyTextReloc; offsets-within-seg are base-independent. gTrampMap.clear(); gTrampList.clear(); gTrampBase = 0; if (segSizes.size() > 1) { std::map symSeg; // name -> (seg, off) std::map symStrong; for (size_t fi = 0; fi < objs.size(); ++fi) { const auto &obj = *objs[fi]; const auto &oo = objOff[fi]; for (const Symbol &sym : obj.symbols) { if (sym.name.empty()) continue; if (sym.shndx == SHN_UNDEF || sym.shndx == SHN_ABS || sym.shndx == SHN_COMMON || sym.shndx >= obj.sections.size()) continue; if (!isLive(fi, sym.shndx)) continue; if (sectionKind(obj.sections[sym.shndx].name) != "text") continue; auto sIt = oo.textSegOf.find(sym.shndx); uint32_t segNum = (sIt == oo.textSegOf.end()) ? 1 : sIt->second; auto wIt = oo.textWithin.find(sym.shndx); uint32_t off = (wIt == oo.textWithin.end() ? 0 : wIt->second) + sym.value; bool thisStrong = (sym.bind != STB_WEAK); auto it = symStrong.find(sym.name); if (it == symStrong.end()) { symSeg[sym.name] = {segNum, off}; symStrong[sym.name] = thisStrong; } else if (thisStrong && !it->second) { symSeg[sym.name] = {segNum, off}; it->second = true; } // weak-after-* / strong-after-strong: keep first } } // Resolve a reloc's target to a (text-seg>1, off) key, matching // resolveSym's section-symbol vs named-symbol handling. auto crossSegFunc = [&](const InputObject &obj, const ObjOffsets &oo, const Reloc &r, TrampKey &key) -> bool { if (r.symIdx >= obj.symbols.size()) return false; const Symbol &sym = obj.symbols[r.symIdx]; if (sym.type == STT_SECTION) { if (sym.shndx >= obj.sections.size()) return false; if (sectionKind(obj.sections[sym.shndx].name) != "text") return false; auto sIt = oo.textSegOf.find(sym.shndx); uint32_t segNum = (sIt == oo.textSegOf.end()) ? 1 : sIt->second; if (segNum <= 1) return false; auto wIt = oo.textWithin.find(sym.shndx); key.seg = segNum; key.off = (wIt == oo.textWithin.end() ? 0 : wIt->second) + (uint32_t)r.addend; return true; } auto it = symSeg.find(sym.name); if (it == symSeg.end() || it->second.seg <= 1) return false; key.seg = it->second.seg; key.off = it->second.off + (uint32_t)r.addend; return true; }; for (size_t fi = 0; fi < objs.size(); ++fi) { const auto &obj = *objs[fi]; const auto &oo = objOff[fi]; for (uint32_t textIdx : obj.sectionsByKind("text")) { if (!isLive(fi, textIdx)) continue; auto rIt = obj.relocs.find(textIdx); if (rIt == obj.relocs.end()) continue; for (const Reloc &r : rIt->second) { if (r.type != R_W65816_IMM16) continue; // Skip INTRA-section refs: asm self-modification // (`sta label+2`), local branch/label address-takes, // any abs operand into the section's OWN code. Those // use the offset with the current bank (DBR/PBR), NOT // the $00BE function-pointer form, so trampolining them // would redirect the write/branch to the table and // break it. A real C &func crosses sections (clang // -ffunction-sections), so it is never skipped here. if (r.symIdx < obj.symbols.size() && obj.symbols[r.symIdx].shndx == textIdx) { if (gReportSelfMod) { const uint8_t *sd = obj.sectionData(textIdx); uint8_t opc = (r.offset >= 1) ? sd[r.offset - 1] : 0; // Absolute STORE opcodes (DBR-relative): sta/ // stx/sty/stz abs, abs,x, abs,y -> self-mod. bool isStore = opc == 0x8D || opc == 0x9D || opc == 0x99 || opc == 0x8E || opc == 0x8C || opc == 0x9C || opc == 0x9E; // Absolute READ opcodes (DBR-relative): a read // of a table embedded in the code's OWN .text // gets the wrong bank when DBR != the code bank // (e.g. code in seg2, DBR=globals seg1). bool isLoad = opc == 0xAD || opc == 0xBD || opc == 0xB9 || opc == 0xAE || opc == 0xBE || opc == 0xAC || opc == 0xBC || opc == 0x0D || opc == 0x2D || opc == 0x4D || opc == 0x6D || opc == 0xCD || opc == 0xED || opc == 0x2C; if (isStore || isLoad) { const Symbol &ts = obj.symbols[r.symIdx]; std::fprintf(stderr, "link816: SELF-REF %s (bank-sensitive): " "%s .text+0x%x opcode 0x%02X -> %s+%d " "[fix: DBR=PBR around it]\n", isStore ? "STORE" : "READ", obj.path.c_str(), r.offset, opc, ts.name.empty() ? "" : ts.name.c_str(), r.addend); } } continue; } TrampKey key{}; if (!crossSegFunc(obj, oo, r, key)) continue; if (gTrampMap.find(key) == gTrampMap.end()) { gTrampMap[key] = (uint32_t)gTrampList.size(); gTrampList.push_back(key); } } } } if (!gTrampList.empty()) { gTrampBase = segSizes[0]; segSizes[0] += kTrampBytes * (uint32_t)gTrampList.size(); // 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). uint32_t textCeiling = fullBank ? kBank0Ceiling : kIoWindowStart; if (segSizes[0] > textCeiling) { char msg[160]; std::snprintf(msg, sizeof(msg), "seg 1 text + cross-segment trampoline table (%u " "bytes) overruns 0x%X", segSizes[0], textCeiling); die(msg); } std::fprintf(stderr, "link816: %zu cross-seg function trampolines (%u bytes) " "in seg 1\n", gTrampList.size(), kTrampBytes * (uint32_t)gTrampList.size()); } } // Build the segment list with bases. std::vector segments; segments.resize(segSizes.size()); segments[0].segNum = 1; segments[0].base = textBase; segments[0].size = segSizes[0]; for (size_t k = 1; k < segSizes.size(); ++k) { segments[k].segNum = static_cast(k + 1); segments[k].base = segmentBankBase + 0x10000u * (k - 1); segments[k].size = segSizes[k]; } Layout L; L.textBase = textBase; L.textSize = segSizes[0]; // segment-1 text size (bank 0) L.bssSize = curBss; L.rodataBase = rodataBase ? rodataBase : (textBase + segSizes[0]); L.rodataSize = curRodata; L.segments = std::move(segments); // Reject a --rodata-base that overlaps text. Without this // check, the gap between text-end and rodata-base goes // negative, the unsigned subtraction wraps to ~4GB, and the // image-write loop creates a multi-gigabyte file with no // diagnostic. Caught while sweeping --rodata-base values // in a strtok layout-sensitivity test. if (rodataBase && L.rodataBase < L.textBase + L.textSize) { char msg[160]; std::snprintf(msg, sizeof(msg), "--rodata-base 0x%X overlaps text 0x%X+%u " "(rodata must start at or after 0x%X)", L.rodataBase, L.textBase, L.textSize, L.textBase + L.textSize); die(msg); } // Hard-fail if text crosses into the IO window. Code there // would fetch instructions from hardware registers. Programs // that grow this big need to split into bank 1 (not currently // supported by this linker). if (!fullBank && overlapsIoWindow(L.textBase, L.textSize) && L.textBase < kIoWindowStart) { char msg[160]; std::snprintf(msg, sizeof(msg), "text [0x%X+%u] crosses IIgs IO window 0x%X-0x%X — " "shrink the program or split into bank 1", L.textBase, L.textSize, kIoWindowStart, kIoWindowEnd - 1); die(msg); } // Auto-skip the IO window if rodata would land there. Loads // from the IO range return hardware register values (and // writes hit the soft switches), so any rodata data that // landed there would silently corrupt at runtime — caught // when math.o grew past ~28KB and pushed string literals into // the IO range, breaking smoke #86 (hash table strcmp // returned garbage because the keys read back as IO register // values). Catches both "starts before IO, crosses in" and // "starts inside IO" cases. if (!fullBank && !rodataBase && overlapsIoWindow(L.rodataBase, L.rodataSize)) { // Page-align upward past the IO window. L.rodataBase = kIoWindowEnd; // Pad the image so the gap between text-end and rodata- // start is just zeros. The runInMame loader skips // writes to the IO range so the soft switches stay // intact. } // .init_array goes immediately after .rodata in the image. L.initBase = L.rodataBase + L.rodataSize; L.initSize = curInit; // Init_array can also land in IO if rodata ends just before // or starts inside. if (!fullBank && overlapsIoWindow(L.initBase, L.initSize)) { L.initBase = kIoWindowEnd; } // After all skips, sanity-check we haven't gone past the LC // ceiling. The IIgs LC area is $D000-$FFFF (12KB usable when // bank 1 is selected; the $E000-$FFFF chunk is common to both // banks). crt0's `lda $C083` read-twice enables RAM read+write // for the entire LC range, so we can use through $FFFF. if (L.initBase + L.initSize > kBank0Ceiling) { char msg[160]; std::snprintf(msg, sizeof(msg), "rodata + init_array [0x%X+%u] exceeds bank-0 LC " "ceiling 0x%X — shrink the runtime or split into bank 1", L.rodataBase, (unsigned)(L.initBase + L.initSize - L.rodataBase), kBank0Ceiling); die(msg); } uint32_t initBase = L.initBase; // bss-base safety: default 0x2000 only works if text doesn't // grow past it. When text + rodata + init_array would // overflow the 0x2000 bss start, shift bss above them so // crt0's bss-init doesn't zero loaded text bytes. Caller // can still force a specific bssBase via --bss-base. // // IIgs bank-0 hazard zones: // $C000-$CFFF: IO and soft switches (ALWAYS unusable — // reads/writes hit hardware registers). // $D000-$DFFF: Language Card 1 area. Read-only ROM by // default; crt0 enables LC1 RAM via the // $C083 soft switch (read-twice trick) so // BSS placed here is writable. // $E000-$FFFF: bank-0 ROM area, also LC-switched but // we don't enable it (less common need). // Skip past the IO window if BSS would land there; LC1 // ($D000-$DFFF) IS now usable thanks to crt0's soft-switch // enable. Above $DFFF means BSS exceeds 16-bit range — // bail clearly rather than silently corrupt. uint32_t loadEnd = L.initBase + L.initSize; L.bssBase = bssBase; // If --bss-base specifies a non-bank-0 address, the user is // explicitly placing BSS in a different bank — don't auto- // adjust around bank-0 hazard zones (IO window, LC1) or // collide with the text/rodata/init load area. Validate the // bank choice + intra-bank fit instead. bool bssOutOfBank0 = (L.bssBase >= 0x010000u); if (!bssOutOfBank0) { if (L.bssBase < loadEnd) { // Page-align upward for nicer addresses in the map. L.bssBase = (loadEnd + 0xFF) & ~0xFFu; if (!fullBank && L.bssBase >= kIoWindowStart && L.bssBase < kIoWindowEnd) { L.bssBase = kIoWindowEnd; } } // Also bump past the IO window if BSS would SPAN it // (starts below kIoWindowStart, extends into or past it). // BSS writes to the IO range hit soft switches — caught // by smoke #128 hex dumper, where ~954-byte BSS pushed // past kIoWindowStart and BSS-clear writes crashed MAME. if (!fullBank && overlapsIoWindow(L.bssBase, L.bssSize)) { L.bssBase = kIoWindowEnd; } if (L.bssBase + L.bssSize > kBank0Ceiling) { char msg[256]; std::snprintf(msg, sizeof(msg), "bss [0x%X+%u] exceeds bank-0 ceiling 0x%X — " "shrink runtime, or pass --bss-base 0xNN0000 " "(multi-bank BSS up to 4 banks now supported)", L.bssBase, L.bssSize, kBank0Ceiling); die(msg); } } else { // Multi-bank BSS: BSS may now span multiple consecutive // banks. crt0 clears bank-by-bank using DBR-relative // STZ abs,X — see __bss_seg_count / __bss_segN_* symbols // below. Capped at 4 segments (= 256KB BSS max) which // covers any realistic IIgs program. uint32_t firstBank = (L.bssBase >> 16) & 0xFF; uint32_t lastBank = ((L.bssBase + L.bssSize - 1) >> 16) & 0xFF; uint32_t segCount = lastBank - firstBank + 1; if (segCount > 4) { char msg[200]; std::snprintf(msg, sizeof(msg), "bss [0x%X+%u] spans %u banks — limit is 4 (256KB)", L.bssBase, L.bssSize, segCount); die(msg); } } // Publish layout now so resolveSym() can read it during reloc // application (it's a const member that uses lastLayout). lastLayout = L; // Synthesize linker-defined symbols so crt0 / startup code // can find the section extents. These must NOT be in the // input objects; we provide them. globalSyms["__text_start"] = L.textBase; globalSyms["__text_end"] = L.textBase + L.textSize; globalSyms["__rodata_start"] = L.rodataBase; globalSyms["__rodata_end"] = L.rodataBase + L.rodataSize; globalSyms["__init_array_start"] = initBase; globalSyms["__init_array_end"] = initBase + curInit; globalSyms["__bss_start"] = L.bssBase; globalSyms["__bss_end"] = L.bssBase + L.bssSize; // Multi-bank-BSS support: split __bss_start into the 16-bit // intra-bank offset and the 8-bit bank byte. crt0 needs the // bank byte separately so it can temporarily set DBR to that // bank for the BSS-clear loop (which uses STZ abs,X — DBR- // relative — and so reads bytes from the wrong bank if BSS // is placed in a non-zero bank). Also emit __bss_size as a // 16-bit count for the loop boundary; doing so saves crt0 // from doing the (__bss_end - __bss_start) subtraction at // runtime, and keeps the count clean even when __bss_start // and __bss_end straddle bank-boundary arithmetic. globalSyms["__bss_lo16"] = L.bssBase & 0xFFFF; globalSyms["__bss_bank"] = (L.bssBase >> 16) & 0xFF; globalSyms["__bss_size"] = L.bssSize <= 0xFFFFu ? L.bssSize : 0xFFFFu; // Multi-bank BSS segment table — up to 4 entries. Each segment // has (lo16, bank, size16). Segment 0 starts at __bss_lo16 in // __bss_bank; segments 1..N-1 start at offset 0 in successive // banks. crt0 walks __bss_seg{0..N-1}_size and skips when 0. { uint32_t curBase = L.bssBase; uint32_t curRem = L.bssSize; uint32_t segIdx = 0; const char *sizeNames[4] = { "__bss_seg0_size", "__bss_seg1_size", "__bss_seg2_size", "__bss_seg3_size" }; const char *bankNames[4] = { "__bss_seg0_bank", "__bss_seg1_bank", "__bss_seg2_bank", "__bss_seg3_bank" }; const char *lo16Names[4] = { "__bss_seg0_lo16", "__bss_seg1_lo16", "__bss_seg2_lo16", "__bss_seg3_lo16" }; // Cap segment size to 0xFF00 (= 65280) so the 16-bit // CPX in crt0 doesn't wrap to 0 on a full-bank segment. // Excess bytes in that bank stay uncleared at link time — // we'd need to chain a second segment in the same bank to // cover them. Implementation: track per-segment max as // 0xFF00, and if a single bank needs more, allocate two // segments in that bank. constexpr uint32_t MAX_SEG = 0xFF00u; for (segIdx = 0; segIdx < 4; segIdx++) { uint32_t bankEnd = (curBase & 0xFF0000u) + 0x10000u; uint32_t avail = bankEnd - curBase; uint32_t seg = curRem < avail ? curRem : avail; if (seg > MAX_SEG) seg = MAX_SEG; globalSyms[lo16Names[segIdx]] = curBase & 0xFFFF; globalSyms[bankNames[segIdx]] = (curBase >> 16) & 0xFF; globalSyms[sizeNames[segIdx]] = seg; curRem -= seg; if (curRem == 0) { segIdx++; break; } curBase += seg; // advance within bank or to next } // Zero out any unused segment slots so crt0 sees size=0. for (uint32_t i = segIdx; i < 4; i++) { globalSyms[lo16Names[i]] = 0; globalSyms[bankNames[i]] = 0; globalSyms[sizeNames[i]] = 0; } } // __heap_start / __heap_end: everything from bss_end to the top // of the entry segment's BANK. // // Under GS/OS there is no IO window and no language card to dodge // here. (A bare-metal link -- crt0.s, loaded at $00:1000 -- DOES put // this window in bank 0, across the $C000-$CFFF I/O page; libc's // heap init stops such a window at $C000 in banks $00/$01/$E0/$E1.) // Those exist only in banks $00/$01/$E0/$E1; GS/OS places bank-aligned // OMF segments in free banks (measured under MAME: joeylib's // examples run in $08-$0B, and bank $00 shows up only in ROM and // GS/OS glue), and crt0Gsos points DBR at the entry segment's // bank, so these offsets address plain RAM across the whole 64K. // // The old model capped the heap at $BF00 ("kBank0HeapTop") and // fell back to a bank-0 language-card range, which cost ~16K of // the entry bank and drove clang-build.sh to shrink --segment-cap // to fit rodata + BSS + heap under a window that is not there. It // also justified itself with "crt0's $C083 read-twice enables // read+write", but crt0Gsos.s does no language-card enable at all // ("GS/OS configures memory"). // // What this window actually does in a program that links libc: // it is a SIZE REQUEST. libc's __heapInitMM asks the Memory // Manager for (heap_end - heap_start) bytes and malloc runs // inside that handle, wherever the MM places it. The window is // only addressed directly if that allocation fails, and on that // path it is still in the entry bank, so it is still plain RAM. // // 0xFFFE rather than 0x10000: relocation patching at the use site // is 16-bit, so 0x10000 truncates to 0 and malloc would compute // heap_end < heap_start and return NULL for everything. constexpr uint32_t kEntryBankHeapTop = 0xFFFE; uint32_t heapStart = L.bssBase + L.bssSize; uint32_t heapEnd = kEntryBankHeapTop; if (heapStart >= heapEnd) { // BSS filled the bank (it may span several). No link-time // window is possible; libc's __heapInitMM sees a zero-size // request and applies its own 32K default, which is the best // available answer. heapEnd = heapStart; } globalSyms["__heap_start"] = heapStart; globalSyms["__heap_end"] = heapEnd; // 2. Build global symbol map. Honor weak vs strong binding: // - strong def overrides any prior weak def // - strong + strong is a multiple-definition error // - weak + weak: first wins (any choice would be valid) // - weak after strong: ignored // Without this, the previous "last def wins" rule meant a weak // libc stub (e.g. putchar) could silently overwrite a user's // strong override depending on link order. std::map isStrong; // name -> strong-def seen for (size_t fi = 0; fi < objs.size(); ++fi) { const auto &obj = *objs[fi]; const auto &oo = objOff[fi]; for (const Symbol &sym : obj.symbols) { if (sym.name.empty()) continue; if (sym.shndx == SHN_UNDEF || sym.shndx == SHN_ABS || sym.shndx == SHN_COMMON || sym.shndx >= obj.sections.size()) continue; // Skip dead sections under gc-sections — their symbols // would otherwise resolve to whatever junk address the // missing oo.{text,rodata,bss,init}Within entry implies. if (!isLive(fi, sym.shndx)) continue; const auto &sec = obj.sections[sym.shndx]; std::string kind = sectionKind(sec.name); uint32_t addr = 0; if (kind == "text") { auto it = oo.textWithin.find(sym.shndx); auto sIt = oo.textSegOf.find(sym.shndx); uint32_t segNum = (sIt == oo.textSegOf.end()) ? 1 : sIt->second; uint32_t segBase = (segNum >= 1 && segNum <= L.segments.size()) ? L.segments[segNum - 1].base : textBase; addr = segBase + (it == oo.textWithin.end() ? 0 : it->second) + sym.value; } else if (kind == "rodata") { auto it = oo.rodataWithin.find(sym.shndx); addr = L.rodataBase + oo.rodataBaseInMerged + (it == oo.rodataWithin.end() ? 0 : it->second) + sym.value; } else if (kind == "bss") { auto it = oo.bssWithin.find(sym.shndx); addr = L.bssBase + oo.bssBaseInMerged + (it == oo.bssWithin.end() ? 0 : it->second) + sym.value; } else if (kind == "init_array") { auto it = oo.initWithin.find(sym.shndx); addr = initBase + oo.initBaseInMerged + (it == oo.initWithin.end() ? 0 : it->second) + sym.value; } else { continue; } if (sym.bind == STB_LOCAL) { // 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); if (sit == isStrong.end()) { globalSyms[sym.name] = addr; isStrong[sym.name] = thisStrong; } else if (thisStrong && !sit->second) { // strong over weak — replace. globalSyms[sym.name] = addr; sit->second = true; } else if (thisStrong && sit->second) { die("multiple strong definitions of '" + sym.name + "'"); } // weak after strong, or weak after weak: keep first. } } // 3. Build per-segment text buffers + rodata. Skip dead // sections under gc-sections. std::vector> segTextBufs(L.segments.size()); for (size_t k = 0; k < L.segments.size(); ++k) segTextBufs[k].reserve(L.segments[k].size); for (size_t fi = 0; fi < objs.size(); ++fi) { const auto &oo = objOff[fi]; for (uint32_t idx : objs[fi]->sectionsByKind("text")) { if (!isLive(fi, idx)) continue; auto sIt = oo.textSegOf.find(idx); uint32_t segNum = (sIt == oo.textSegOf.end()) ? 1 : sIt->second; const uint8_t *p = objs[fi]->sectionData(idx); auto &buf = segTextBufs[segNum - 1]; buf.insert(buf.end(), p, p + objs[fi]->sections[idx].size); } } // ---- Emit the cross-seg trampoline table at the end of seg 1's text. // Each entry is `JML func` (0x5C + 24-bit target). Under ExpressLoad // the target is zeroed and its cINTERSEG site makes the Loader patch // func's runtime bank:offset; a bare-metal link's segment bases are // final, so the absolute target is baked in. // segTextBufs[0] holds exactly gTrampBase bytes here (all seg-1 text), // so entry i lands at offset gTrampBase + 4*i, and applyTextReloc // redirects each cross-seg IMM16 &func to that offset. if (!gTrampList.empty()) { auto &b0 = segTextBufs[0]; for (size_t i = 0; i < gTrampList.size(); ++i) { uint32_t tgt = gMultiSegRelocMode ? 0u : L.segments[gTrampList[i].seg - 1].base + gTrampList[i].off; b0.push_back(0x5C); // JML abs-long opcode b0.push_back(static_cast(tgt & 0xFF)); b0.push_back(static_cast((tgt >> 8) & 0xFF)); b0.push_back(static_cast((tgt >> 16) & 0xFF)); if (gRecordSites) { InterImm24Site s; s.patchSeg = 1; s.patchOff = static_cast(gTrampBase + 4 * i + 1); s.targetSeg = static_cast(gTrampList[i].seg); s.targetOff = static_cast(gTrampList[i].off); s.byteCnt = 3; s.bitShift = 0; gInterImm24Sites.push_back(s); } } } std::vector rodataBuf; rodataBuf.reserve(curRodata); for (size_t fi = 0; fi < objs.size(); ++fi) { for (uint32_t idx : objs[fi]->sectionsByKind("rodata")) { if (!isLive(fi, idx)) continue; const uint8_t *p = objs[fi]->sectionData(idx); rodataBuf.insert(rodataBuf.end(), p, p + objs[fi]->sections[idx].size); } } // 4. Apply relocations to text buffers (each in its own segment). for (size_t fi = 0; fi < objs.size(); ++fi) { const auto &obj = *objs[fi]; const auto &oo = objOff[fi]; for (uint32_t textIdx : obj.sectionsByKind("text")) { if (!isLive(fi, textIdx)) continue; auto it = obj.relocs.find(textIdx); if (it == obj.relocs.end()) continue; auto sIt = oo.textSegOf.find(textIdx); uint32_t segNum = (sIt == oo.textSegOf.end()) ? 1 : sIt->second; uint32_t inSeg = oo.textWithin.at(textIdx); uint32_t segBase = L.segments[segNum - 1].base; auto &textBuf = segTextBufs[segNum - 1]; for (const Reloc &r : it->second) { uint32_t patchOff = inSeg + r.offset; uint32_t patchAddr = segBase + patchOff; uint32_t target; std::string 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 // is intra-bank only). Detect and report so the // user can adjust packing. IMM16 cross-bank is // tolerated: 16-bit absolute uses DBR for the // 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. 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; if (targetSegBank != patchSegBank) { char msg[200]; std::snprintf(msg, sizeof(msg), "%s: cross-bank PCREL reloc to '%s' (target bank " "0x%X, code bank 0x%X) — adjust --segment-cap " "or pack referenced section into the same segment", obj.path.c_str(), resolvedName.c_str(), targetSegBank, patchSegBank); die(msg); } } applyTextReloc(textBuf, patchOff, patchAddr, target, r.type, resolvedName, segNum, L.segments, targetWeakNull); } } } // Move per-segment patched text into the Layout for output. for (size_t k = 0; k < L.segments.size(); ++k) L.segments[k].body = std::move(segTextBufs[k]); // Re-publish layout now that segment bodies are populated — // writeMultiSegment reads from lastLayout. lastLayout = L; // 4b. Apply relocations to rodata/data buffer. Globals like // `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]; for (uint32_t rdIdx : obj.sectionsByKind("rodata")) { if (!isLive(fi, rdIdx)) continue; auto it = obj.relocs.find(rdIdx); if (it == obj.relocs.end()) continue; uint32_t inMerged = oo.rodataBaseInMerged + oo.rodataWithin.at(rdIdx); for (const Reloc &r : it->second) { uint32_t patchOff = inMerged + r.offset; uint32_t patchAddr = L.rodataBase + patchOff; uint32_t target; std::string resolvedName; bool targetWeakNull = false; if (!resolveSym(obj, oo, r, target, resolvedName, &targetWeakNull)) die(obj.path + ": .rodata reloc to unresolved '" + 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); } } } // 5. Compose output: segment-1 text || (gap) || rodata. // bss is virtual. Multi-segment builds emit additional text // segments separately (see writeSegmentImages); the main -o // output stays segment 1's image so existing single-segment // smoke checks still work unchanged. outImage.clear(); const uint32_t seg1TextSize = static_cast(L.segments[0].body.size()); outImage = L.segments[0].body; if (L.rodataBase != textBase + seg1TextSize) { uint32_t gap = L.rodataBase - (textBase + seg1TextSize); outImage.insert(outImage.end(), gap, 0); } outImage.insert(outImage.end(), rodataBuf.begin(), rodataBuf.end()); // Build init_array buffer + apply its relocations (entries are // 16-bit function pointers needing IMM16 reloc). std::vector initBuf; initBuf.reserve(curInit); for (size_t fi = 0; fi < objs.size(); ++fi) { for (uint32_t idx : objs[fi]->sectionsByKind("init_array")) { if (!isLive(fi, idx)) continue; const uint8_t *p = objs[fi]->sectionData(idx); initBuf.insert(initBuf.end(), p, p + objs[fi]->sections[idx].size); } } for (size_t fi = 0; fi < objs.size(); ++fi) { const auto &obj = *objs[fi]; const auto &oo = objOff[fi]; for (uint32_t idx : obj.sectionsByKind("init_array")) { if (!isLive(fi, idx)) continue; auto it = obj.relocs.find(idx); if (it == obj.relocs.end()) continue; uint32_t inMerged = oo.initBaseInMerged + oo.initWithin.at(idx); for (const Reloc &r : it->second) { uint32_t target; std::string resolvedName; if (!resolveSym(obj, oo, r, target, resolvedName)) die(obj.path + ": .init_array reloc to unresolved '" + resolvedName + "'"); uint32_t patchOff = inMerged + r.offset; uint32_t patchAddr = initBase + patchOff; applyReloc(initBuf, patchOff, patchAddr, target, r.type, resolvedName); } } } outImage.insert(outImage.end(), initBuf.begin(), initBuf.end()); return L; } // ---------------------------------------------------------------- // DWARF sidecar. Walks each input object and concatenates every // section whose name starts with `.debug_`. Each section is // prefixed by an ASCII-readable header line: // // ; OBJ SEC SIZE RELOCS // // followed by the section bytes after applying any text/rodata/bss/ // init_array relocations from `.rela.`. This means PCs in // .debug_info / .debug_line / .debug_aranges resolve to final-image // addresses and a consumer like llvm-dwarfdump (or a custom MAME // overlay) can map them back to source lines. // // Intra-debug references (e.g., .debug_info -> .debug_str offsets) // are *not* renumbered; we concatenate sections without recompacting, // so the original object-local offsets stay correct relative to each // object's slice of the sidecar. A multi-TU consumer would need to // walk the slice headers to find the right base. void writeDebugSidecar(const std::string &path) const { std::ofstream f(path, std::ios::binary); if (!f) die("cannot open '" + path + "' for writing"); f << "; llvm816 link816 DWARF sidecar v1\n"; f << "; text/rodata/bss/init_array relocs applied to final-image addresses\n"; f << "; intra-debug refs left object-local (per-OBJ slice scope)\n"; size_t total = 0; size_t kept = 0; size_t patched = 0; for (size_t fi = 0; fi < objs.size(); ++fi) { const InputObject &obj = *objs[fi]; const ObjOffsets &oo = objOff[fi]; for (uint32_t idx = 0; idx < obj.sections.size(); ++idx) { const Section &sec = obj.sections[idx]; if (sec.name.rfind(".debug_", 0) != 0) continue; if (sec.size == 0) continue; std::vector data(sec.size); std::memcpy(data.data(), obj.raw.data() + sec.fileOffset, sec.size); size_t applied = 0; size_t skipped = 0; auto it = obj.relocs.find(idx); if (it != obj.relocs.end()) { for (const Reloc &r : it->second) { uint32_t target; std::string resolvedName; if (!resolveSym(obj, oo, r, target, resolvedName)) { skipped++; continue; } uint32_t w = relocWidth(r.type); if (w == 0 || r.offset + w > sec.size) { // Unknown reloc type, or offset+width // would walk off the section end. // Defensively skip. skipped++; continue; } // patchAddr is only meaningful for PCREL types. // DWARF .debug_* sections don't get a runtime // load address, so PCREL within debug is // structurally weird (the assembler converts // intra-section diffs to PCREL, but the // resulting displacement is sidecar-relative, // not runtime-relative). Pass 0 — same as // the prior behaviour. applyReloc ignores // patchAddr for absolute DATA32 / IMMNN. applyReloc(data, r.offset, 0, target, r.type, resolvedName); applied++; } } patched += applied; char hdr[256]; std::snprintf(hdr, sizeof(hdr), "; OBJ %s SEC %s SIZE %u RELOCS_APPLIED %zu RELOCS_SKIPPED %zu\n", obj.path.c_str(), sec.name.c_str(), sec.size, applied, skipped); f.write(hdr, std::strlen(hdr)); f.write(reinterpret_cast(data.data()), sec.size); f << "\n"; total += sec.size; kept++; } } std::fprintf(stderr, "debug sidecar: %zu sections, %zu bytes, %zu relocs applied -> %s\n", kept, total, patched, path.c_str()); } void writeMap(const std::string &path) const { std::ofstream f(path); if (!f) die("cannot open '" + path + "' for writing"); char buf[256]; // Section layout summary at top. std::snprintf(buf, sizeof(buf), "# section layout\n" ".text : 0x%06x .. 0x%06x (%6u bytes)\n" ".rodata : 0x%06x .. 0x%06x (%6u bytes)\n" ".bss : 0x%06x .. 0x%06x (%6u bytes)\n", lastLayout.textBase, lastLayout.textBase + lastLayout.textSize, lastLayout.textSize, lastLayout.rodataBase, lastLayout.rodataBase + lastLayout.rodataSize, lastLayout.rodataSize, lastLayout.bssBase, lastLayout.bssBase + lastLayout.bssSize, lastLayout.bssSize); f.write(buf, std::strlen(buf)); // Per-input-file contributions to .text (size in bytes). std::snprintf(buf, sizeof(buf), "\n# per-input-file .text contributions\n"); f.write(buf, std::strlen(buf)); for (size_t fi = 0; fi < objs.size(); ++fi) { uint32_t bytes = 0; for (uint32_t idx : objs[fi]->sectionsByKind("text")) bytes += objs[fi]->sections[idx].size; std::snprintf(buf, sizeof(buf), "%6u %s\n", bytes, objs[fi]->path.c_str()); f.write(buf, std::strlen(buf)); } // Symbol table sorted by address. std::snprintf(buf, sizeof(buf), "\n# global symbols (sorted by address)\n"); f.write(buf, std::strlen(buf)); std::vector> sorted; for (const auto &kv : globalSyms) sorted.emplace_back(kv.second, kv.first); std::sort(sorted.begin(), sorted.end()); for (const auto &p : sorted) { std::snprintf(buf, sizeof(buf), "0x%06x %s\n", p.first, p.second.c_str()); f.write(buf, std::strlen(buf)); } // Backwards-compat: also emit the old `name = 0x...` lines so // existing smoke greps still match. for (const auto &kv : globalSyms) { std::snprintf(buf, sizeof(buf), "%s = 0x%06x\n", kv.first.c_str(), kv.second); f.write(buf, std::strlen(buf)); } // Optional STB_LOCAL section. Gated by --map-locals because the // pc2line.py funcAt() resolver matches "0x... name" lines anywhere // in the file; adding locals unconditionally would change function // attribution for any tool that reads the map without expecting // local names. When the flag is on, emit a `# local symbols` // banner + the same `0x... name` line format used for globals, // but with the @objfile suffix stripped (so pc2line sees the // bare symbol name). The profiler is the primary consumer. if (mapLocals && !localSyms.empty()) { std::snprintf(buf, sizeof(buf), "\n# local symbols (sorted by address)\n"); f.write(buf, std::strlen(buf)); std::vector> localsSorted; for (const auto &kv : localSyms) localsSorted.emplace_back(kv.second, kv.first); std::sort(localsSorted.begin(), localsSorted.end()); for (const auto &p : localsSorted) { // Strip "@objpath" disambiguation suffix for pc2line. std::string nm = p.second; size_t at = nm.find('@'); if (at != std::string::npos) nm = nm.substr(0, at); std::snprintf(buf, sizeof(buf), "0x%06x %s\n", p.first, nm.c_str()); f.write(buf, std::strlen(buf)); } } } // Write per-segment images for segments 2..N (segment 1 is the // main -o output) and a JSON manifest describing all segments. // Image filename convention: .seg.bin where outBase is // the -o path with any trailing extension stripped. Manifest JSON // is at the user-supplied --manifest path. void writeMultiSegment(const std::string &mainOutPath, const std::string &mfPath, const std::string &entrySym) const { if (lastLayout.segments.empty()) return; // Strip the extension from mainOutPath for per-segment names. std::string outBase = mainOutPath; size_t dot = outBase.find_last_of('.'); size_t slash = outBase.find_last_of('/'); if (dot != std::string::npos && (slash == std::string::npos || dot > slash)) { outBase = outBase.substr(0, dot); } // Per-segment images for K >= 2. for (size_t k = 1; k < lastLayout.segments.size(); ++k) { const auto &seg = lastLayout.segments[k]; char name[256]; std::snprintf(name, sizeof(name), "%s.seg%u.bin", outBase.c_str(), seg.segNum); std::ofstream f(name, std::ios::binary); if (!f) die(std::string("cannot open '") + name + "' for writing"); f.write(reinterpret_cast(seg.body.data()), seg.body.size()); } // Manifest. Hand-rolled JSON (no external dep). if (mfPath.empty()) return; std::ofstream mf(mfPath); if (!mf) die("cannot open '" + mfPath + "' for writing"); // Sized for the per-segment block below plus a long image path; the // block's fixed text is ~250 bytes, leaving generous path headroom. char buf[1024]; std::snprintf(buf, sizeof(buf), "{\n" " \"version\": 1,\n" " \"main\": \"%s\",\n" " \"entry\": \"%s\",\n" " \"segments\": [\n", mainOutPath.c_str(), entrySym.c_str()); mf << buf; for (size_t k = 0; k < lastLayout.segments.size(); ++k) { const auto &seg = lastLayout.segments[k]; std::string imgPath = mainOutPath; if (k > 0) { char nm[256]; std::snprintf(nm, sizeof(nm), "%s.seg%u.bin", outBase.c_str(), seg.segNum); imgPath = nm; } uint32_t entryOff = 0; // Set entry_offset on whichever segment actually contains // the entry symbol — usually segment 1 (crt0's __start) // but could be any segment if user picks a non-standard // entry point. auto it = globalSyms.find(entrySym); if (it != globalSyms.end() && it->second >= seg.base && 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; // omfEmit embeds this segment's BSS as trailing LCONST zeros, so // BSS must live in THIS segment's bank. Multi-bank BSS // (--bss-base in a higher bank) cannot be embedded here, and // crt0Gsos relies on the Loader's LCONST fill (it runs no // bank-by-bank STZ clear like the bare-metal/GNO path), so the // out-of-bank BSS would be neither reserved nor zeroed. Reject // the combination loudly rather than emit a broken file. if (segBssSize > 0u && (segBssStart & 0xFF0000u) != (seg.base & 0xFF0000u)) { die("multi-segment ExpressLoad build has BSS outside the " "text segment's bank; multi-bank BSS + --manifest is not " "supported (BSS cannot be embedded or zeroed for GS/OS)"); } 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; } mf << " ]\n}\n"; } // Stash the last layout so writeMap can use it. Layout lastLayout; }; // ---------------------------------------------------------------- CLI static uint32_t parseInt(const std::string &s) { char *end = nullptr; unsigned long v = std::strtoul(s.c_str(), &end, 0); if (end == s.c_str() || *end != '\0') die("bad numeric value '" + s + "'"); // 65816 addresses are 24-bit; reject anything that doesn't fit so // a typo like `--text-base 0x100000000` doesn't silently wrap to 0. if (v > 0xFFFFFF) die("address '" + s + "' exceeds 24-bit range"); return static_cast(v); } static void usage(const char *argv0) { std::fprintf(stderr, "usage: %s -o [--text-base ADDR] [--rodata-base ADDR]\n" " [--bss-base ADDR] [--map FILE] [--map-locals]\n" " [--debug-out FILE] [--reloc-out FILE]\n" " [--no-gc-sections] [--filetype N] [--aux N]\n" " ...\n" "\n" " --reloc-out FILE write IMM24 relocation site list (binary:\n" " ...)\n" " consumed by omfEmit --relocs to emit cRELOC\n" " opcodes for runtime bank-byte fixup.\n" " --filetype N ProDOS filetype (0..0xFF) for the OMF. $B3=S16\n" " (GS/OS app), $B5=EXE (GNO shell command). Writes\n" " .meta sidecar consumed by disk-image\n" " builders.\n" " --aux N ProDOS auxtype (0..0xFFFF). Typically 0 for\n" " compliant programs; $DC00 = GNO non-compliant\n" " (bypasses interrupt-driven keyboard buffer).\n", argv0); std::exit(2); } } // anonymous namespace int main(int argc, char **argv) { std::string outPath; std::string mapPath; std::string debugOutPath; std::string relocOutPath; Linker linker; int i = 1; while (i < argc) { std::string a = argv[i]; if (a == "-o" || a == "--output") { if (++i >= argc) usage(argv[0]); outPath = argv[i++]; } else if (a == "--text-base") { if (++i >= argc) usage(argv[0]); linker.textBase = parseInt(argv[i++]); } else if (a == "--rodata-base") { if (++i >= argc) usage(argv[0]); linker.rodataBase = parseInt(argv[i++]); } else if (a == "--bss-base") { if (++i >= argc) usage(argv[0]); linker.bssBase = parseInt(argv[i++]); } else if (a == "--map") { if (++i >= argc) usage(argv[0]); mapPath = argv[i++]; } else if (a == "--map-locals") { // Augment --map output with STB_LOCAL symbols. Required for // function-attribution profiling so PC samples that fall into // libgcc helpers / file-static functions resolve to a name. linker.mapLocals = true; i++; } else if (a == "--debug-out") { if (++i >= argc) usage(argv[0]); debugOutPath = argv[i++]; } else if (a == "--reloc-out") { if (++i >= argc) usage(argv[0]); relocOutPath = argv[i++]; } else if (a == "--gc-sections") { // GC of unreachable sections is on by default; --gc-sections // is accepted as a no-op alias for clarity. Use // --no-gc-sections to disable. i++; } else if (a == "--no-gc-sections") { linker.gcSections = false; i++; } else if (a == "--segment-cap") { if (++i >= argc) usage(argv[0]); linker.segmentCap = parseInt(argv[i++]); } else if (a == "--full-bank") { linker.fullBank = true; i++; } else if (a == "--overflow-cap") { if (++i >= argc) usage(argv[0]); linker.overflowCap = parseInt(argv[i++]); } else if (a == "--segment-bank-base") { if (++i >= argc) usage(argv[0]); linker.segmentBankBase = parseInt(argv[i++]); } 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 == "--report-self-mod") { gReportSelfMod = true; i++; } else if (a == "--filetype") { if (++i >= argc) usage(argv[0]); linker.fileType = (int32_t)parseInt(argv[i++]); if (linker.fileType < 0 || linker.fileType > 0xFF) die("--filetype must be 0..0xFF"); } else if (a == "--aux") { if (++i >= argc) usage(argv[0]); linker.auxType = (int32_t)parseInt(argv[i++]); if (linker.auxType < 0 || linker.auxType > 0xFFFF) die("--aux must be 0..0xFFFF"); } else if (a == "-h" || a == "--help") { usage(argv[0]); } else if (!a.empty() && a[0] == '-') { die("unknown option '" + a + "'"); } else { linker.addObject(a); i++; } } if (outPath.empty() || linker.objs.empty()) usage(argv[0]); // Enable IMM24 site recording before linking, so applyReloc populates // gImm24Sites for cRELOC sidecar emission. if (!relocOutPath.empty()) { 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); std::ofstream f(outPath, std::ios::binary); if (!f) die("cannot open '" + outPath + "' for writing"); f.write(reinterpret_cast(image.data()), image.size()); // ProDOS filetype/aux sidecar. Emit as `.meta` — simple // line-oriented `key: 0xVALUE` format consumed by the disk-image // builder (cppo/Cadius/our scripts). Default filetype if not // specified: 0xB3 (S16) for single-segment, the multi-segment // builder writes 0xB3 anyway. if (linker.fileType >= 0 || linker.auxType >= 0) { std::string metaPath = outPath + ".meta"; std::ofstream mf(metaPath); if (!mf) die("cannot open '" + metaPath + "' for writing"); if (linker.fileType >= 0) mf << "filetype: 0x" << std::hex << linker.fileType << "\n"; if (linker.auxType >= 0) mf << "aux: 0x" << std::hex << linker.auxType << "\n"; } if (!mapPath.empty()) linker.writeMap(mapPath); if (!debugOutPath.empty()) linker.writeDebugSidecar(debugOutPath); if (!relocOutPath.empty()) { // Single-flat sidecar binary format (v3): // u32 count // { u32 patchOff; u32 offsetRef; u8 byteCnt; u8 bitShift; // u8 pad[2]; } × count // Both offsets are within the segment image (== link-time addr // minus textBase). byteCnt = 2 for IMM16 or BANK16, 3 for IMM24. // bitShift = 0 normally, 16 for BANK16 (so the Loader patches // 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(); rf.write(reinterpret_cast(&count), 4); for (const auto &s : gImm24Sites) { uint32_t po = s.patchOff, off = s.offsetRef; uint8_t bc = s.byteCnt, bs = s.bitShift, pad[2] = {0, 0}; rf.write(reinterpret_cast(&po), 4); rf.write(reinterpret_cast(&off), 4); rf.write(reinterpret_cast(&bc), 1); 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. if (L.segments.size() > 1 || !linker.manifestPath.empty()) { // Default entry symbol is __start (crt0's program entry, // which calls main). GS/OS Loader runs from segment 1's // entry; crt0 lives in segment 1 by convention (first // input object is typically the runtime/crt0). linker.writeMultiSegment(outPath, linker.manifestPath, "__start"); } std::fprintf(stderr, "link816: %lu bank0-data cINTERSEG sites\n", gBank0DataSites); std::fprintf(stderr, "linked: text=[0x%04x+%u] rodata=[0x%04x+%u] bss=[0x%04x+%u] " "-> %s (%zu bytes)", L.textBase, L.textSize, L.rodataBase, L.rodataSize, L.bssBase, L.bssSize, outPath.c_str(), image.size()); if (L.segments.size() > 1) { std::fprintf(stderr, " [dbg: textReloc=%lu bare=%lu]", gDbgTextCalls, gDbgBareCalls); std::fprintf(stderr, " + %zu extra segments", L.segments.size() - 1); for (size_t k = 1; k < L.segments.size(); ++k) { std::fprintf(stderr, " seg%u=[0x%06x+%zu]", L.segments[k].segNum, L.segments[k].base, L.segments[k].body.size()); } } std::fprintf(stderr, "\n"); return 0; }