2697 lines
132 KiB
C++
2697 lines
132 KiB
C++
// 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;
|
||
}
|