65816-llvm-mos/src/link816/link816.cpp
2026-10-13 13:58:56 -05:00

2697 lines
132 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
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<uint8_t> readFile(const std::string &path) {
std::ifstream f(path, std::ios::binary);
if (!f) die("cannot open '" + path + "' for reading");
std::vector<uint8_t> buf((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
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.<anything>` 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<uint32_t>(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<uint8_t> raw;
std::vector<Section> sections;
std::vector<Symbol> symbols;
// relocs indexed by target section id
std::map<uint32_t, std::vector<Reloc>> 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<const char *>(
raw.data() + shstrhdr.sh_offset);
sections.resize(hdr.e_shnum);
std::vector<Elf32Shdr> 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<const char *>(
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<Reloc> &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<uint8_t>(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<uint32_t> sectionsByKind(const std::string &kind) const {
std::vector<uint32_t> 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<uint32_t>(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<uint8_t> 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<TextSeg> 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<Imm24Site> 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<uint32_t, std::vector<Imm24Site>> gIntraImm24SitesPerSeg;
static std::vector<InterImm24Site> 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<TrampKey, uint32_t> gTrampMap; // key -> trampoline index
static std::vector<TrampKey> 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<TextSeg> &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<uint8_t> &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<uint8_t>(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<uint8_t>(target & 0xFF);
buf[off + 1] = static_cast<uint8_t>((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<uint8_t>((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<uint8_t>(target & 0xFF);
buf[off + 1] = static_cast<uint8_t>((target >> 8) & 0xFF);
buf[off + 2] = static_cast<uint8_t>((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<int64_t>(target) - (static_cast<int64_t>(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<uint8_t>(pcrelDisp & 0xFF);
break;
case R_W65816_PCREL16:
pcrelDisp = static_cast<int64_t>(target) - (static_cast<int64_t>(patchAddr) + 2);
if (pcrelDisp < -32768 || pcrelDisp > 32767)
die("R_W65816_PCREL16 to '" + symName +
"' out of BRL range");
buf[off] = static_cast<uint8_t>(pcrelDisp & 0xFF);
buf[off + 1] = static_cast<uint8_t>((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<uint8_t>(target & 0xFF);
buf[off + 1] = static_cast<uint8_t>((target >> 8) & 0xFF);
buf[off + 2] = static_cast<uint8_t>((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<int64_t>(target) - (static_cast<int64_t>(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<uint8_t>(pcrelDisp & 0xFF);
buf[off + 1] = static_cast<uint8_t>((pcrelDisp >> 8) & 0xFF);
buf[off + 2] = static_cast<uint8_t>((pcrelDisp >> 16) & 0xFF);
buf[off + 3] = static_cast<uint8_t>((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<uint8_t> &buf, uint32_t off,
uint32_t patchAddr, uint32_t target,
uint8_t rtype,
const std::string &symName,
uint32_t patchSeg,
const std::vector<TextSeg> &segments,
bool targetIsWeakNull = false) {
// UNDEFINED-WEAK reference resolved to absolute 0 (resolveSym's
// STB_WEAK branch). This is the `if (fp) fp();` null-stub pattern:
// e.g. libc's getchar() does `if (__getByte) return __getByte();`
// against the weak, link-time-unresolved __getByte console hook, so
// clang emits a 2-byte near JSR whose IMM16 operand is the hook's
// address -- 0 -- guarded by a null test that is dead at runtime.
//
// Two things force special handling here rather than the normal
// classification below:
// 1. The baked operand MUST stay literally 0 so the `if (fp)` test
// keeps failing. A cRELOC/cINTERSEG that rewrote those bytes to
// a placed segment base would make the null test pass and run the
// dead call into garbage. So we emit NO relocation record.
// 2. With --text-base 0 the null address (0) falls inside segment
// 1's [base, base+size) range, so findSegContaining() reports
// targetSeg == 1. A patch site in an overflow segment
// (patchSeg > 1) then looks like a cross-segment reference and
// the 2-byte IMM16/PCREL form is rejected further down (a near
// JSR has no room for the bank byte the Loader would need). With
// a non-zero --text-base the same reference resolves cleanly via
// the targetSeg == 0 path; baking the literal 0 here restores
// that behaviour regardless of the text base or the patch seg.
//
// Bake the literal little-endian value (0, or 0 + a weak addend) into
// the absolute/data operand and return with no side effects. PCREL
// forms (a branch to a null symbol) are nonsensical and never weak-
// null in practice, so they fall through to the normal path.
if (targetIsWeakNull) {
switch (rtype) {
case R_W65816_IMM8:
case R_W65816_IMM16:
case R_W65816_BANK16:
case R_W65816_IMM24:
case R_W65816_DATA32: {
uint32_t w = relocWidth(rtype);
for (uint32_t i = 0; i < w; ++i)
buf[off + i] = static_cast<uint8_t>((target >> (8 * i)) & 0xFF);
return;
}
default:
break;
}
}
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<uint16_t>(patchSeg);
s.patchOff = static_cast<uint16_t>(patchAddr - pseg.base);
s.targetSeg = 1;
s.targetOff = static_cast<uint16_t>(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<uint16_t>(patchSeg);
s.patchOff = static_cast<uint16_t>(patchAddr - pseg.base);
s.targetSeg = static_cast<uint8_t>(targetSeg);
s.targetOff = static_cast<uint16_t>(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<std::unique_ptr<InputObject>> 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<size_t, uint32_t>;
std::set<SecID> liveSecs;
std::map<std::string, SecID> 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<std::string, bool> 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<SecID> 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<uint32_t, uint32_t> textWithin; // offset within its segment
std::map<uint32_t, uint32_t> textSegOf; // section idx -> segment num (1-based)
std::map<uint32_t, uint32_t> rodataWithin;
std::map<uint32_t, uint32_t> bssWithin;
std::map<uint32_t, uint32_t> initWithin;
};
std::vector<ObjOffsets> objOff;
std::map<std::string, uint32_t> 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<std::string, uint32_t> 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<uint8_t> 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<InputObject>();
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<uint8_t> &data) {
size_t pos = 8; // past "!<arch>\n"
std::string longNames; // "//" GNU long-name table
size_t members = 0;
while (pos + 60 <= data.size()) {
const char *hdr = reinterpret_cast<const char *>(&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<size_t>(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<const char *>(&data[dataOff]),
static_cast<size_t>(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<size_t>(
std::strtoul(name.c_str() + 1, nullptr, 10));
name.clear(); // "/<offset>" -> 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<InputObject>();
o->path = path + "(" + name + ")";
o->raw.assign(data.begin() + dataOff,
data.begin() + dataOff +
static_cast<size_t>(msize));
o->parse();
objs.push_back(std::move(o));
++members;
}
pos = dataOff + static_cast<size_t>(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<uint8_t> &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<uint32_t> 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<std::string, TrampKey> symSeg; // name -> (seg, off)
std::map<std::string, bool> 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() ? "<sec>" : 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<TextSeg> 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<uint32_t>(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<std::string, bool> 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<std::vector<uint8_t>> 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<uint8_t>(tgt & 0xFF));
b0.push_back(static_cast<uint8_t>((tgt >> 8) & 0xFF));
b0.push_back(static_cast<uint8_t>((tgt >> 16) & 0xFF));
if (gRecordSites) {
InterImm24Site s;
s.patchSeg = 1;
s.patchOff = static_cast<uint16_t>(gTrampBase + 4 * i + 1);
s.targetSeg = static_cast<uint8_t>(gTrampList[i].seg);
s.targetOff = static_cast<uint16_t>(gTrampList[i].off);
s.byteCnt = 3;
s.bitShift = 0;
gInterImm24Sites.push_back(s);
}
}
}
std::vector<uint8_t> 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<uint32_t>(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<uint8_t> 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 <objname> SEC <sectionname> SIZE <bytes> RELOCS <n>
//
// followed by the section bytes after applying any text/rodata/bss/
// init_array relocations from `.rela.<sec>`. 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<uint8_t> 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<const char *>(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<std::pair<uint32_t, std::string>> 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<std::pair<uint32_t, std::string>> 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: <outBase>.seg<N>.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<const char *>(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<uint32_t>(v);
}
static void usage(const char *argv0) {
std::fprintf(stderr,
"usage: %s -o <output> [--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"
" <input.o> ...\n"
"\n"
" --reloc-out FILE write IMM24 relocation site list (binary:\n"
" <count:u32><patchOff:u32 offsetRef:u32>...)\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"
" <output>.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<uint8_t> 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<const char *>(image.data()), image.size());
// ProDOS filetype/aux sidecar. Emit as `<output>.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:
// <relocOutPath>.seg<N>.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<const char *>(&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<const char *>(&po), 4);
rf.write(reinterpret_cast<const char *>(&off), 4);
rf.write(reinterpret_cast<const char *>(&bc), 1);
rf.write(reinterpret_cast<const char *>(&bs), 1);
rf.write(reinterpret_cast<const char *>(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<const char *>(&intraCount), 4);
pf.write(reinterpret_cast<const char *>(&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<const char *>(&po), 4);
pf.write(reinterpret_cast<const char *>(&off), 4);
pf.write(reinterpret_cast<const char *>(&bc), 1);
pf.write(reinterpret_cast<const char *>(&bs), 1);
pf.write(reinterpret_cast<const char *>(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<const char *>(&po), 4);
pf.write(reinterpret_cast<const char *>(&to), 4);
pf.write(reinterpret_cast<const char *>(&ts), 1);
pf.write(reinterpret_cast<const char *>(&bc), 1);
pf.write(reinterpret_cast<const char *>(&bs), 1);
pf.write(reinterpret_cast<const char *>(&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;
}