65816-llvm-mos/src/link816/link816.cpp
2026-05-18 14:43:35 -05:00

1572 lines
71 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.
//
// 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
//
// 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;
};
static constexpr uint32_t SHT_NULL = 0;
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;
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;
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;
static constexpr uint8_t STB_GLOBAL = 1;
static constexpr uint8_t STB_WEAK = 2;
static constexpr uint8_t STT_NOTYPE = 0;
static constexpr uint8_t STT_OBJECT = 1;
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;
// ---------------------------------------------------------------- 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;
}
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. Same for .fini_array (destructors).
if (name == ".init_array" || name.rfind(".init_array.", 0) == 0) return "init_array";
if (name == ".fini_array" || name.rfind(".fini_array.", 0) == 0) return "fini_array";
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));
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 void applyReloc(std::vector<uint8_t> &buf, uint32_t off,
uint32_t patchAddr, uint32_t target,
uint8_t rtype, const std::string &symName) {
int64_t Signed;
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).
if (gRecordSites) {
uint32_t targetBank = target & 0xFF0000;
uint32_t baseBank = gTextBaseForSites & 0xFF0000;
if (targetBank == baseBank) {
Imm24Site s;
s.patchOff = patchAddr - gTextBaseForSites;
s.offsetRef = target - gTextBaseForSites;
// Use type field width = 2 to distinguish from IMM24
// (3). Imm24Site struct is reused — emitOmf will
// emit cRELOC ByteCnt=2 for this.
s.byteCnt = 2;
s.bitShift = 0;
gImm24Sites.push_back(s);
}
}
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;
if (gRecordSites) {
uint32_t targetBank = target & 0xFF0000;
uint32_t baseBank = gTextBaseForSites & 0xFF0000;
if (targetBank == baseBank) {
Imm24Site s;
s.patchOff = patchAddr - gTextBaseForSites;
s.offsetRef = target - gTextBaseForSites;
s.byteCnt = 2;
s.bitShift = 16;
gImm24Sites.push_back(s);
}
}
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);
// Record the site for OMF cRELOC emission (only if recording is
// enabled — gRecordSites is set by the CLI when --reloc-out is
// requested). The patch offset is within the segment image; the
// reference offset is the in-segment offset of the target.
if (gRecordSites) {
// 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.
uint32_t targetBank = target & 0xFF0000;
uint32_t baseBank = gTextBaseForSites & 0xFF0000;
if (targetBank == baseBank) {
Imm24Site s;
s.patchOff = patchAddr - gTextBaseForSites;
s.offsetRef = target - gTextBaseForSites;
s.byteCnt = 3;
s.bitShift = 0;
gImm24Sites.push_back(s);
}
}
break;
case R_W65816_PCREL8:
Signed = static_cast<int64_t>(target) - (static_cast<int64_t>(patchAddr) + 1);
if (Signed < -128 || Signed > 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)Signed);
die(msg);
}
buf[off] = static_cast<uint8_t>(Signed & 0xFF);
break;
case R_W65816_PCREL16:
Signed = static_cast<int64_t>(target) - (static_cast<int64_t>(patchAddr) + 2);
if (Signed < -32768 || Signed > 32767)
die("R_W65816_PCREL16 to '" + symName +
"' out of BRL range");
buf[off] = static_cast<uint8_t>(Signed & 0xFF);
buf[off + 1] = static_cast<uint8_t>((Signed >> 8) & 0xFF);
break;
default: {
char msg[128];
std::snprintf(msg, sizeof(msg),
"unhandled relocation type %u to '%s'", rtype, symName.c_str());
die(msg);
}
}
}
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;
uint32_t segmentBankBase = 0x040000;
std::string manifestPath;
// 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;
void addObject(const std::string &path) {
auto o = std::make_unique<InputObject>();
o->path = path;
o->raw = readFile(path);
o->parse();
objs.push_back(std::move(o));
}
// 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.
bool resolveSym(const InputObject &obj, const ObjOffsets &oo,
const Reloc &r,
uint32_t &target, std::string &resolvedName) const {
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 {
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;
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();
for (size_t fi = 0; fi < objs.size(); ++fi) {
ObjOffsets &oo = objOff[fi];
oo.textBaseInMerged = curText;
for (uint32_t idx : objs[fi]->sectionsByKind("text")) {
if (!isLive(fi, idx)) continue;
uint32_t sz = objs[fi]->sections[idx].size;
// If adding this section would exceed the cap, start a
// new segment. Skip empty sections in the cap check
// (they fit anywhere). Sections larger than the cap
// get their own segment (we don't split a single
// section across banks — it'd violate intra-section
// PCREL and 16-bit absolute addressing).
if (segmentCap && sz > 0 &&
segSizes[curSeg - 1] > 0 &&
segSizes[curSeg - 1] + sz > segmentCap) {
curSeg++;
segSizes.push_back(0);
}
oo.textSegOf[idx] = curSeg;
oo.textWithin[idx] = segSizes[curSeg - 1];
segSizes[curSeg - 1] += sz;
curText += sz;
}
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;
}
}
// 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 ($C000-$CFFF).
// 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 (L.textBase < 0xC000 &&
L.textBase + L.textSize > 0xC000) {
char msg[160];
std::snprintf(msg, sizeof(msg),
"text [0x%X+%u] crosses IIgs IO window 0xC000-0xCFFF — "
"shrink the program or split into bank 1",
L.textBase, L.textSize);
die(msg);
}
// Auto-skip the IO window ($C000-$CFFF) if rodata would land
// there. Loads from $C000-$CFFF 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 (!rodataBase &&
L.rodataBase < 0xD000 &&
L.rodataBase + L.rodataSize > 0xC000) {
// Page-align upward past the IO window.
L.rodataBase = 0xD000;
// 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 (L.initBase < 0xD000 &&
L.initBase + L.initSize > 0xC000) {
L.initBase = 0xD000;
}
// 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 > 0x10000u) {
char msg[160];
std::snprintf(msg, sizeof(msg),
"rodata + init_array [0x%X+%u] exceeds bank-0 LC "
"ceiling 0x10000 — shrink the runtime or split into bank 1",
L.rodataBase,
(unsigned)(L.initBase + L.initSize - L.rodataBase));
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 (L.bssBase >= 0xC000 && L.bssBase < 0xD000) {
L.bssBase = 0xD000;
}
}
if (L.bssBase + L.bssSize > 0x10000u) {
char msg[256];
std::snprintf(msg, sizeof(msg),
"bss [0x%X+%u] exceeds bank-0 ceiling 0x10000 — "
"shrink runtime, or pass --bss-base 0xNN0000 "
"(multi-bank BSS up to 4 banks now supported)",
L.bssBase, L.bssSize);
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
if ((curBase & 0xFFFFu) == 0) {
// Crossed bank boundary — already at start of next bank.
} else if ((curBase & 0xFF0000u) != ((curBase - 1) & 0xFF0000u)) {
// Just crossed into next bank.
}
}
// 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: pick the largest contiguous safe
// range above bss_end. Without this, the previous hardcoded
// heap_end=$BF00 gave heap_end < heap_start whenever BSS
// spilled into LC1 — malloc immediately returned NULL.
// If bank-0 heap would be tiny (<512B) push to LC1 ($D000+).
uint32_t heapStart = L.bssBase + L.bssSize;
constexpr uint32_t MIN_HEAP = 512;
if (heapStart >= 0xBF00 && heapStart < 0xD000) {
heapStart = 0xD000; // skip IO window + tiny tail
} else if (heapStart < 0xBF00 && (0xBF00 - heapStart) < MIN_HEAP) {
heapStart = 0xD000; // bank-0 sliver too small; use LC
}
globalSyms["__heap_start"] = heapStart;
if (heapStart < 0xC000) {
globalSyms["__heap_end"] = 0xBF00;
} else if (heapStart < 0x10000u) {
// Heap in LC area ($D000-$FFFF). crt0's $C083 read-twice
// enables read+write for the whole range. Cap at 0xFFFE
// (not 0x10000) — relocation patching at the use site is
// 16-bit and 0x10000 truncates to 0; malloc would then
// think heap_end < heap_start and return NULL.
globalSyms["__heap_end"] = 0xFFFE;
} else {
// Unreachable — bssBase + bssSize > 0x10000 check above.
globalSyms["__heap_end"] = heapStart;
}
// 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;
}
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);
}
}
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;
if (!resolveSym(obj, oo, r, target, resolvedName))
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.
if (segmentCap && (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);
}
}
applyReloc(textBuf, patchOff, patchAddr, target, r.type,
resolvedName);
}
}
}
// 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;` need their initializer patched at link time
// (the .o emits a placeholder 0 + a R_W65816_IMM16 reloc).
// Without this, every initialized pointer or function-pointer
// table in the program reads 0 at runtime.
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;
if (!resolveSym(obj, oo, r, target, resolvedName))
die(obj.path + ": .rodata reloc to unresolved '"
+ resolvedName + "'");
applyReloc(rodataBuf, patchOff, patchAddr, target,
r.type, resolvedName);
}
}
}
// 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;
}
if (r.offset + 3 > sec.size) {
// Out-of-range offset; defensively skip.
skipped++;
continue;
}
// patchAddr is only meaningful for PCREL types,
// which DWARF doesn't use. Pass 0; applyReloc
// ignores it for absolute types.
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));
}
}
// 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");
char buf[512];
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;
}
std::snprintf(buf, sizeof(buf),
" {\n"
" \"num\": %u,\n"
" \"name\": \"SEG%u\",\n"
" \"base\": \"0x%06x\",\n"
" \"size\": %zu,\n"
" \"image\": \"%s\",\n"
" \"entry_offset\": \"0x%04x\"\n"
" }%s\n",
seg.segNum, seg.segNum, seg.base, seg.body.size(),
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] [--debug-out FILE]\n"
" [--reloc-out FILE] [--no-gc-sections]\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",
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 == "--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") {
// Drop sections not reachable from __start / main /
// init_array. Requires `-ffunction-sections` (so each
// function is in its own section). Significantly shrinks
// text for programs that link the whole runtime but only
// use a fraction of it. ON by default; --no-gc-sections
// disables.
linker.gcSections = true;
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 == "--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 == "-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();
}
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());
if (!mapPath.empty()) linker.writeMap(mapPath);
if (!debugOutPath.empty()) linker.writeDebugSidecar(debugOutPath);
if (!relocOutPath.empty()) {
// 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.
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);
}
}
// 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,
"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, " + %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;
}