More linker and loader fixes.

This commit is contained in:
Scott Duensing 2026-06-27 19:30:35 -05:00
parent 7f0c50304a
commit eabfaf3fcb
4 changed files with 288 additions and 12 deletions

View file

@ -76,11 +76,24 @@ __gsosIsRealImpl:
; The macro generates a unique pbLabel-suffixed `.long 0` slot whose ; The macro generates a unique pbLabel-suffixed `.long 0` slot whose
; low 24 bits get patched by the prologue, leaving the pad byte at 0. ; low 24 bits get patched by the prologue, leaving the pad byte at 0.
.macro gsosDispatch callNum, pbLabel .macro gsosDispatch callNum, pbLabel
; The two `sta \pbLabel` stores self-modify the INLINE pBlock slot
; below, which lives in THIS wrapper's code bank (PBR). They must
; target PBR, not DBR (the caller's data/globals bank): under multi-
; segment this wrapper can land in seg2 while DBR=seg1, so a DBR-
; relative store would patch the wrong bank, leaving the inline
; `.long 0` unpatched -> GS/OS dispatches on a null pBlock. phb saves
; the caller DBR; phk/plb sets DBR:=PBR for the patch; the final plb
; restores the caller DBR before the dispatch. A (offset) and X (bank)
; are untouched by phb/phk/plb. Stack is balanced before the jsl.
phb
phk
plb
sta \pbLabel sta \pbLabel
sep #0x20 sep #0x20
txa txa
sta \pbLabel+2 sta \pbLabel+2
rep #0x20 rep #0x20
plb
jsl 0xe100a8 jsl 0xe100a8
.word \callNum .word \callNum
\pbLabel: \pbLabel:

View file

@ -55,13 +55,16 @@
; built S16 apps with C++ ctors crash during the init_array ; built S16 apps with C++ ctors crash during the init_array
; indirect-dispatch loop for exactly that reason. ; indirect-dispatch loop for exactly that reason.
; ;
; Constraint preserved: the JMP-indirect anchor must be in bank 0 (the ; Constraint preserved: the long-indirect anchor must be in bank 0 (the
; 65816 JMP (abs) opcode 0x6C reads its PC vector from bank-0 ; 65816 JML [abs] opcode 0xDC reads its 24-bit PC vector from bank-0
; unconditionally). DP is always in bank 0 on the 65816, so ; unconditionally). DP is always in bank 0 on the 65816, so
; (DP+$B8) is automatically a bank-0 address. The target that gets ; (DP+$B8) is automatically a bank-0 address. The fetched 24-bit
; jumped to runs in the current PBR (intra-bank function pointer); ; pointer (offset $B8-$B9 + bank $BA, both stored by clang at the call
; cross-bank function pointers need a different dispatch (the Phase ; site) is honored BANK-EXPLICIT, so a cross-bank function pointer --
; C.2 cINTERSEG path on the JSL, not on this vector). ; e.g. &func of a function the linker placed in a different segment/
; bank, redirected through a seg1 JML trampoline -- lands in the
; correct bank. When bank==PBR (single-segment) this is byte-for-byte
; equivalent to the old 0x6C/16-bit dispatch.
; ;
; SMC patch site: crt0 variants compute (DP + $B8) at startup with ; SMC patch site: crt0 variants compute (DP + $B8) at startup with
; tdc ; clc ; adc #0xb8 ; sta __jsl_indir_op ; tdc ; clc ; adc #0xb8 ; sta __jsl_indir_op
@ -74,9 +77,13 @@ __indirTarget = 0xb8
.text .text
.globl __jsl_indir .globl __jsl_indir
__jsl_indir: __jsl_indir:
.byte 0x6c ; JMP (abs) opcode (Apple 65816 uses 16-bit operand; .byte 0xdc ; JML [abs] opcode -- 24-bit LONG indirect. Reads a
; the resulting effective address is bank-0:operand, ; 24-bit pointer (offset $B8-$B9 + bank $BA) from
; from which the 16-bit target PC is read). ; bank-0:operand and jumps BANK-EXPLICIT, so a
; cross-bank function pointer lands in the right bank.
; (Was 0x6C JMP(abs), 16-bit/PBR-local; clang already
; stores the pointer's bank byte to $BA, so this just
; honors it. Identical to the old form when bank==PBR.)
.globl __jsl_indir_op .globl __jsl_indir_op
__jsl_indir_op: __jsl_indir_op:
.byte 0xb8, 0x00 ; operand: bank-0 address of the target slot. .byte 0xb8, 0x00 ; operand: bank-0 address of the target slot.

View file

@ -432,6 +432,16 @@ static std::vector<Imm24Site> gImm24Sites;
static uint32_t gTextBaseForSites = 0; static uint32_t gTextBaseForSites = 0;
static bool gRecordSites = false; static bool gRecordSites = false;
// --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 // Phase C.2 — per-segment + cross-segment IMM24 site tracking, used
// when multi-segment ExpressLoad OMFs need the GS/OS Loader to patch // when multi-segment ExpressLoad OMFs need the GS/OS Loader to patch
// cross-seg JSLs after picking banks dynamically. // cross-seg JSLs after picking banks dynamically.
@ -457,6 +467,30 @@ static std::map<uint32_t, std::vector<Imm24Site>> gIntraImm24SitesPerSeg;
static std::vector<InterImm24Site> gInterImm24Sites; static std::vector<InterImm24Site> gInterImm24Sites;
static bool gMultiSegRelocMode = false; // set when text layout has >1 segment 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 // Record an intra-segment patch site for cRELOC emission. A target
// below the text base is never intra-segment (it is an undefined-weak // below the text base is never intra-segment (it is an undefined-weak
@ -688,6 +722,26 @@ static void applyTextReloc(std::vector<uint8_t> &buf, uint32_t off,
} }
const TextSeg &pseg = segments[patchSeg - 1]; const TextSeg &pseg = segments[patchSeg - 1];
uint32_t targetSeg = findSegContaining(target, segments); 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).
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;
}
}
// Non-text target (rodata / bss / init_array / weak-zero) — use // Non-text target (rodata / bss / init_array / weak-zero) — use
// the legacy applyReloc path (which records via recordCRelocSite // the legacy applyReloc path (which records via recordCRelocSite
// into gImm24Sites for back-compat AND also pushes into the new // into gImm24Sites for back-compat AND also pushes into the new
@ -1102,6 +1156,148 @@ struct Linker {
curInit += objs[fi]->sections[idx].size; curInit += objs[fi]->sections[idx].size;
} }
} }
// ---- TRAMPOLINE pre-pass (multi-seg 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 (gMultiSegRelocMode) {
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();
if (segmentCap && segSizes[0] > segmentCap)
die("cross-segment trampoline table pushes seg 1 over "
"--segment-cap");
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. // Build the segment list with bases.
std::vector<TextSeg> segments; std::vector<TextSeg> segments;
segments.resize(segSizes.size()); segments.resize(segSizes.size());
@ -1467,6 +1663,29 @@ struct Linker {
buf.insert(buf.end(), p, p + objs[fi]->sections[idx].size); 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, zeroed); its cINTERSEG
// site makes the Loader patch the target to func's runtime bank:offset.
// 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) {
b0.push_back(0x5C); // JML abs-long opcode
b0.push_back(0); b0.push_back(0); b0.push_back(0); // 24-bit tgt
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; std::vector<uint8_t> rodataBuf;
rodataBuf.reserve(curRodata); rodataBuf.reserve(curRodata);
for (size_t fi = 0; fi < objs.size(); ++fi) { for (size_t fi = 0; fi < objs.size(); ++fi) {
@ -1953,6 +2172,9 @@ int main(int argc, char **argv) {
} else if (a == "--multi-seg-expressload") { } else if (a == "--multi-seg-expressload") {
linker.multiSegExpressLoad = true; linker.multiSegExpressLoad = true;
i++; i++;
} else if (a == "--report-self-mod") {
gReportSelfMod = true;
i++;
} else if (a == "--filetype") { } else if (a == "--filetype") {
if (++i >= argc) usage(argv[0]); if (++i >= argc) usage(argv[0]);
linker.fileType = (int32_t)parseInt(argv[i++]); linker.fileType = (int32_t)parseInt(argv[i++]);

View file

@ -270,7 +270,18 @@ static std::vector<uint8_t> emitOneSeg(const std::vector<uint8_t> &image,
// Earlier I tried 0 (matched one decoded file) but real // Earlier I tried 0 (matched one decoded file) but real
// executable code segments use 0x10000. // executable code segments use 0x10000.
const uint32_t BANKSIZE = 0x10000; const uint32_t BANKSIZE = 0x10000;
const uint32_t ALIGN = 0; // ALIGN = 0x10000 — BANK-ALIGN the segment. This is REQUIRED for the
// multi-segment model: link816 lays out each segment at a bank base
// (--text-base 0 / --segment-bank-base) and the code uses 16-bit self-
// references (computed-goto `rts` tables, intra-segment branches) whose
// operands equal the link offset. Those only resolve correctly when the
// System Loader places the segment at bank:0000 so runtime_offset ==
// link_offset. With ALIGN=0 the Loader puts a segment wherever the
// Memory Manager has room (e.g. $07:BB11) -> every computed-goto jumps to
// bank:link_offset instead of bank:(base+link_offset) -> wild crash.
// (KIND=0x1000 / ABSBANK only means "fits in one bank", NOT "bank-
// aligned"; the ALIGN field is what forces alignment.)
const uint32_t ALIGN = 0x10000;
const uint8_t NUMSEX = 0; const uint8_t NUMSEX = 0;
std::vector<uint8_t> hdr; std::vector<uint8_t> hdr;
@ -521,9 +532,20 @@ static std::vector<uint8_t> emitOmfExpressLoad(
userSegs.reserve(users.size()); userSegs.reserve(users.size());
for (size_t k = 0; k < users.size(); k++) { for (size_t k = 0; k < users.size(); k++) {
const auto &u = users[k]; const auto &u = users[k];
// In a MULTI-segment file the MAIN/root segment (first user seg =
// the one the Loader JSLs to) must carry the DYNAMIC attribute
// (0x0100) on top of the CODE kind -- a real GS/OS multi-seg app
// (the Finder's root FINDER seg) is KIND=0x1100 while its other
// segs are 0x1000. Without DYNAMIC on the root, the GS/OS Loader
// rejects the whole multi-seg file (no crt0 lands in any bank).
// Single-segment files keep the plain 0x1000 (known-good; one seg,
// no root marker needed).
const uint16_t segKind = (k == 0 && users.size() > 1)
? (uint16_t)(OMF_KIND_CODE_PRIV | 0x0100)
: OMF_KIND_CODE_PRIV;
userSegs.push_back(emitOneSeg(u.image, u.entryOffset, /*org*/0, userSegs.push_back(emitOneSeg(u.image, u.entryOffset, /*org*/0,
/*segNum*/(uint16_t)(k + 2), /*segNum*/(uint16_t)(k + 2),
/*kind*/OMF_KIND_CODE_PRIV, /*kind*/segKind,
u.name, u.bssSize, u.bssGap, u.name, u.bssSize, u.bssGap,
u.relocSites, u.interSites)); u.relocSites, u.interSites));
} }
@ -624,6 +646,13 @@ static std::vector<uint8_t> emitOmfExpressLoad(
// N = total segments in the file (including ExpressLoad). For a // N = total segments in the file (including ExpressLoad). For a
// 1-user-seg layout count=0 (N=2) or count=1 (N=3 with DP/Stack); // 1-user-seg layout count=0 (N=2) or count=1 (N=3 with DP/Stack);
// for K user segs count = K - 1 (+ 1 if DP/Stack). // for K user segs count = K - 1 (+ 1 if DP/Stack).
// NOTE 2026-06-26: tried count=nSegs and count=0 -- neither makes a
// multi-seg ExpressLoad OMF load under real GS/OS (the Loader rejects
// the whole file regardless of count). The real blocker is upstream:
// the multi-seg segments carry NO cRELOC/cINTERSEG records and NO
// embedded BSS (the --manifest path never wires link816's reloc/inter
// sites or bssSize into the UserSegs), unlike the single-seg --relocs
// path. Completing that pipeline is the open multi-seg work.
put32(elData, 0); // reserved put32(elData, 0); // reserved
put16(elData, (uint16_t)(nSegs - 1)); // count = N-2 = nSegs-1 put16(elData, (uint16_t)(nSegs - 1)); // count = N-2 = nSegs-1
@ -956,7 +985,12 @@ int main(int argc, char **argv) {
// { u32 patchOff; u32 offsetRef; u8 byteCnt; u8 bitShift; u8 pad[2]; } // { u32 patchOff; u32 offsetRef; u8 byteCnt; u8 bitShift; u8 pad[2]; }
// v2 used pad[3]=0 — v3 keeps the same size by repurposing pad[0] as // v2 used pad[3]=0 — v3 keeps the same size by repurposing pad[0] as
// bitShift. v2 sidecars are read transparently (bitShift = pad[0]=0). // bitShift. v2 sidecars are read transparently (bitShift = pad[0]=0).
if (!relocFile.empty()) { //
// In MANIFEST mode --relocs names the per-seg sidecar BASE
// (<base>.seg<N>.reloc), read later per UserSeg; the flat file here is
// the single-seg format and would abort on a multi-seg build's absolute
// offsets, so skip it when a manifest is in play.
if (!relocFile.empty() && manifest.empty()) {
auto raw = readFile(relocFile); auto raw = readFile(relocFile);
if (raw.size() < 4) die("--relocs file too small"); if (raw.size() < 4) die("--relocs file too small");
uint32_t cnt = (uint32_t)raw[0] | ((uint32_t)raw[1] << 8) uint32_t cnt = (uint32_t)raw[0] | ((uint32_t)raw[1] << 8)