diff --git a/runtime/src/iigsGsos.s b/runtime/src/iigsGsos.s index b6350c9..35d3345 100644 --- a/runtime/src/iigsGsos.s +++ b/runtime/src/iigsGsos.s @@ -76,11 +76,24 @@ __gsosIsRealImpl: ; 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. .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 sep #0x20 txa sta \pbLabel+2 rep #0x20 + plb jsl 0xe100a8 .word \callNum \pbLabel: diff --git a/runtime/src/libgcc.s b/runtime/src/libgcc.s index e0b1636..744b838 100644 --- a/runtime/src/libgcc.s +++ b/runtime/src/libgcc.s @@ -55,13 +55,16 @@ ; built S16 apps with C++ ctors crash during the init_array ; indirect-dispatch loop for exactly that reason. ; -; Constraint preserved: the JMP-indirect anchor must be in bank 0 (the -; 65816 JMP (abs) opcode 0x6C reads its PC vector from bank-0 +; Constraint preserved: the long-indirect anchor must be in bank 0 (the +; 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 -; (DP+$B8) is automatically a bank-0 address. The target that gets -; jumped to runs in the current PBR (intra-bank function pointer); -; cross-bank function pointers need a different dispatch (the Phase -; C.2 cINTERSEG path on the JSL, not on this vector). +; (DP+$B8) is automatically a bank-0 address. The fetched 24-bit +; pointer (offset $B8-$B9 + bank $BA, both stored by clang at the call +; site) is honored BANK-EXPLICIT, so a cross-bank function pointer -- +; 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 ; tdc ; clc ; adc #0xb8 ; sta __jsl_indir_op @@ -74,9 +77,13 @@ __indirTarget = 0xb8 .text .globl __jsl_indir __jsl_indir: - .byte 0x6c ; JMP (abs) opcode (Apple 65816 uses 16-bit operand; - ; the resulting effective address is bank-0:operand, - ; from which the 16-bit target PC is read). + .byte 0xdc ; JML [abs] opcode -- 24-bit LONG indirect. Reads a + ; 24-bit pointer (offset $B8-$B9 + bank $BA) from + ; 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 __jsl_indir_op: .byte 0xb8, 0x00 ; operand: bank-0 address of the target slot. diff --git a/src/link816/link816.cpp b/src/link816/link816.cpp index 3ad21b8..4cb55ad 100644 --- a/src/link816/link816.cpp +++ b/src/link816/link816.cpp @@ -432,6 +432,16 @@ static std::vector gImm24Sites; static uint32_t gTextBaseForSites = 0; 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 // when multi-segment ExpressLoad OMFs need the GS/OS Loader to patch // cross-seg JSLs after picking banks dynamically. @@ -457,6 +467,30 @@ static std::map> gIntraImm24SitesPerSeg; static std::vector gInterImm24Sites; static bool gMultiSegRelocMode = false; // set when text layout has >1 segment +// ---- Cross-segment function-pointer TRAMPOLINE table (GS/OS jump-table +// model, all-resident form). clang forms &func as a 16-bit offset + the bank +// from $00BE (the LOAD/seg1 bank), so &func of a function the linker placed +// in seg2+ (a different bank) gets the WRONG (seg1) bank and crashes when the +// pointer is called. Fix: for each cross-seg-function IMM16 address-take, +// emit a `JML func` trampoline in SEG1 (offset + $BE seg1 bank are then +// correct) and redirect the IMM16 to it; the JML's 24-bit target is cINTERSEG +// -relocated so the Loader patches the real seg2 bank. Calling through the +// pointer reaches the seg1 trampoline and tail-jumps (JML) into the function. +// (The matching call-site half -- libgcc __jsl_indir widened to JML[$00B8], +// 24-bit -- lets the dispatch honor the bank.) Keyed by the target's +// (segNum, offsetWithinSeg) so one trampoline serves every reference. +struct TrampKey { + uint32_t seg; + uint32_t off; + bool operator<(const TrampKey &o) const { + return seg != o.seg ? seg < o.seg : off < o.off; + } +}; +static std::map gTrampMap; // key -> trampoline index +static std::vector gTrampList; // index -> key (emit order) +static uint32_t gTrampBase = 0; // byte offset of the table within seg1 +static constexpr uint32_t kTrampBytes = 4; // JML abs-long: 0x5C + 24-bit + // Record an intra-segment patch site for cRELOC emission. A target // below the text base is never intra-segment (it is an undefined-weak @@ -688,6 +722,26 @@ static void applyTextReloc(std::vector &buf, uint32_t off, } const TextSeg &pseg = segments[patchSeg - 1]; 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 // the legacy applyReloc path (which records via recordCRelocSite // into gImm24Sites for back-compat AND also pushes into the new @@ -1102,6 +1156,148 @@ struct Linker { 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 symSeg; // name -> (seg, off) + std::map symStrong; + for (size_t fi = 0; fi < objs.size(); ++fi) { + const auto &obj = *objs[fi]; + const auto &oo = objOff[fi]; + for (const Symbol &sym : obj.symbols) { + if (sym.name.empty()) continue; + if (sym.shndx == SHN_UNDEF || sym.shndx == SHN_ABS || + sym.shndx == SHN_COMMON || sym.shndx >= obj.sections.size()) + continue; + if (!isLive(fi, sym.shndx)) continue; + if (sectionKind(obj.sections[sym.shndx].name) != "text") continue; + auto sIt = oo.textSegOf.find(sym.shndx); + uint32_t segNum = (sIt == oo.textSegOf.end()) ? 1 : sIt->second; + auto wIt = oo.textWithin.find(sym.shndx); + uint32_t off = (wIt == oo.textWithin.end() ? 0 : wIt->second) + + sym.value; + bool thisStrong = (sym.bind != STB_WEAK); + auto it = symStrong.find(sym.name); + if (it == symStrong.end()) { + symSeg[sym.name] = {segNum, off}; + symStrong[sym.name] = thisStrong; + } else if (thisStrong && !it->second) { + symSeg[sym.name] = {segNum, off}; + it->second = true; + } // weak-after-* / strong-after-strong: keep first + } + } + // Resolve a reloc's target to a (text-seg>1, off) key, matching + // resolveSym's section-symbol vs named-symbol handling. + auto crossSegFunc = [&](const InputObject &obj, const ObjOffsets &oo, + const Reloc &r, TrampKey &key) -> bool { + if (r.symIdx >= obj.symbols.size()) return false; + const Symbol &sym = obj.symbols[r.symIdx]; + if (sym.type == STT_SECTION) { + if (sym.shndx >= obj.sections.size()) return false; + if (sectionKind(obj.sections[sym.shndx].name) != "text") + return false; + auto sIt = oo.textSegOf.find(sym.shndx); + uint32_t segNum = (sIt == oo.textSegOf.end()) ? 1 : sIt->second; + if (segNum <= 1) return false; + auto wIt = oo.textWithin.find(sym.shndx); + key.seg = segNum; + key.off = (wIt == oo.textWithin.end() ? 0 : wIt->second) + + (uint32_t)r.addend; + return true; + } + auto it = symSeg.find(sym.name); + if (it == symSeg.end() || it->second.seg <= 1) return false; + key.seg = it->second.seg; + key.off = it->second.off + (uint32_t)r.addend; + return true; + }; + for (size_t fi = 0; fi < objs.size(); ++fi) { + const auto &obj = *objs[fi]; + const auto &oo = objOff[fi]; + for (uint32_t textIdx : obj.sectionsByKind("text")) { + if (!isLive(fi, textIdx)) continue; + auto rIt = obj.relocs.find(textIdx); + if (rIt == obj.relocs.end()) continue; + for (const Reloc &r : rIt->second) { + if (r.type != R_W65816_IMM16) continue; + // Skip INTRA-section refs: asm self-modification + // (`sta label+2`), local branch/label address-takes, + // any abs operand into the section's OWN code. Those + // use the offset with the current bank (DBR/PBR), NOT + // the $00BE function-pointer form, so trampolining them + // would redirect the write/branch to the table and + // break it. A real C &func crosses sections (clang + // -ffunction-sections), so it is never skipped here. + if (r.symIdx < obj.symbols.size() && + obj.symbols[r.symIdx].shndx == textIdx) { + if (gReportSelfMod) { + const uint8_t *sd = obj.sectionData(textIdx); + uint8_t opc = (r.offset >= 1) ? sd[r.offset - 1] : 0; + // Absolute STORE opcodes (DBR-relative): sta/ + // stx/sty/stz abs, abs,x, abs,y -> self-mod. + bool isStore = + opc == 0x8D || opc == 0x9D || opc == 0x99 || + opc == 0x8E || opc == 0x8C || + opc == 0x9C || opc == 0x9E; + // Absolute READ opcodes (DBR-relative): a read + // of a table embedded in the code's OWN .text + // gets the wrong bank when DBR != the code bank + // (e.g. code in seg2, DBR=globals seg1). + bool isLoad = + opc == 0xAD || opc == 0xBD || opc == 0xB9 || + opc == 0xAE || opc == 0xBE || + opc == 0xAC || opc == 0xBC || + opc == 0x0D || opc == 0x2D || opc == 0x4D || + opc == 0x6D || opc == 0xCD || opc == 0xED || + opc == 0x2C; + if (isStore || isLoad) { + const Symbol &ts = obj.symbols[r.symIdx]; + std::fprintf(stderr, + "link816: SELF-REF %s (bank-sensitive): " + "%s .text+0x%x opcode 0x%02X -> %s+%d " + "[fix: DBR=PBR around it]\n", + isStore ? "STORE" : "READ", + obj.path.c_str(), r.offset, opc, + ts.name.empty() ? "" : ts.name.c_str(), + r.addend); + } + } + continue; + } + TrampKey key{}; + if (!crossSegFunc(obj, oo, r, key)) continue; + if (gTrampMap.find(key) == gTrampMap.end()) { + gTrampMap[key] = (uint32_t)gTrampList.size(); + gTrampList.push_back(key); + } + } + } + } + if (!gTrampList.empty()) { + gTrampBase = segSizes[0]; + segSizes[0] += kTrampBytes * (uint32_t)gTrampList.size(); + 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. std::vector segments; segments.resize(segSizes.size()); @@ -1467,6 +1663,29 @@ struct Linker { 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(gTrampBase + 4 * i + 1); + s.targetSeg = static_cast(gTrampList[i].seg); + s.targetOff = static_cast(gTrampList[i].off); + s.byteCnt = 3; + s.bitShift = 0; + gInterImm24Sites.push_back(s); + } + } + } std::vector rodataBuf; rodataBuf.reserve(curRodata); for (size_t fi = 0; fi < objs.size(); ++fi) { @@ -1953,6 +2172,9 @@ int main(int argc, char **argv) { } 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++]); diff --git a/src/link816/omfEmit.cpp b/src/link816/omfEmit.cpp index dc09215..6736dcd 100644 --- a/src/link816/omfEmit.cpp +++ b/src/link816/omfEmit.cpp @@ -270,7 +270,18 @@ static std::vector emitOneSeg(const std::vector &image, // Earlier I tried 0 (matched one decoded file) but real // executable code segments use 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; std::vector hdr; @@ -521,9 +532,20 @@ static std::vector emitOmfExpressLoad( userSegs.reserve(users.size()); for (size_t k = 0; k < users.size(); 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, /*segNum*/(uint16_t)(k + 2), - /*kind*/OMF_KIND_CODE_PRIV, + /*kind*/segKind, u.name, u.bssSize, u.bssGap, u.relocSites, u.interSites)); } @@ -624,6 +646,13 @@ static std::vector emitOmfExpressLoad( // 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); // 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 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]; } // 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). - if (!relocFile.empty()) { + // + // In MANIFEST mode --relocs names the per-seg sidecar BASE + // (.seg.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); if (raw.size() < 4) die("--relocs file too small"); uint32_t cnt = (uint32_t)raw[0] | ((uint32_t)raw[1] << 8)