// omfEmit — wrap a flat binary (or a multi-segment manifest from // link816) in an Apple IIgs OMF v2.1 container. // // Single-segment mode (legacy): one CODE segment with KIND=0, // no INTERSEG opcodes, ORG=0 (loader picks bank). Header layout // per OMF 2.1 spec: 44-byte fixed header + 10-byte LOAD_NAME + // 32-byte SEG_NAME, then the body (DS opcode for the payload, // END opcode terminator). // // omfEmit --input flat.bin --map flat.map --base 0x8000 // --entry main --output prog.omf [--name SEG] // // Multi-segment mode: read the JSON manifest emitted by // `link816 --manifest`, write one OMF segment per manifest entry. // Each segment's ORG is set to its declared base (bank-aligned) // so the loader places it at the exact address the linker assumed // when it patched intra-segment IMM24 / IMM16 relocations. KIND // uses the STATIC + ABSBANK attributes to ask the loader not to // move segments around — necessary because all relocs were already // baked in at link time (no INTERSEG opcodes emitted yet). // // omfEmit --manifest manifest.json --output prog.omf #include #include #include #include #include #include #include #include namespace { // OMF v2.1 protocol constants -- single source of truth for the header // layout and opcode set. See Apple IIgs Tech Note #17 and the FTN // reference. Don't renumber; values are shared with the loader. static constexpr uint8_t OMF_OP_LCONST = 0xF2; static constexpr uint8_t OMF_OP_CRELOC = 0xF5; static constexpr uint8_t OMF_OP_END = 0x00; static constexpr uint8_t OMF_OP_CINTERSEG = 0xF6; [[maybe_unused]] static constexpr uint8_t OMF_NUMLEN = 4; [[maybe_unused]] static constexpr uint8_t OMF_VERSION_V21 = 0x02; [[maybe_unused]] static constexpr uint32_t OMF_HDR_SIZE = 44; [[maybe_unused]] static constexpr uint32_t OMF_LABLEN_FIXED = 10; static constexpr uint16_t OMF_KIND_CODE_PRIV = 0x1000; static constexpr uint16_t OMF_KIND_DPSTACK = 0x4012; // DP/Stack | RELOAD; matches real-world GNO/ME ~_STACK format static constexpr uint16_t OMF_KIND_DATA_STATIC = 0x8001; static constexpr uint16_t OMF_KIND_CODE_STATIC_ABSBANK = 0x8800; // cRELOC opcode wire size: opcode + ByteCnt + BitShift + OffsetPatch + // OffsetReference = 1 + 1 + 1 + 2 + 2 = 7 bytes per site. static constexpr uint32_t OMF_CRELOC_BYTES_PER_SITE = 7; [[noreturn]] static void die(const std::string &msg) { std::fprintf(stderr, "omfEmit: %s\n", msg.c_str()); std::exit(1); } // Populated by --relocs from a link816 sidecar. Each entry is // (OffsetPatch, OffsetReference, ByteCnt) — the in-segment offset // to patch, the in-segment offset of the target, and the byte width // of the patch (2 for IMM16, 3 for IMM24). Consumed by emitOneSeg // to write cRELOC opcodes between LCONST and END. struct RelocSite { uint16_t patchOff; uint16_t offsetRef; uint8_t byteCnt; uint8_t bitShift; // 0 for offset relocs, 16 for BANK16 }; std::vector gReloc24Sites; // Phase C.2 — inter-segment IMM24 site for cINTERSEG (0xF6) emission. // Populated from link816's per-seg sidecar (`.seg.reloc`) // when --manifest+--expressload is in effect. struct InterRelocSite { uint16_t patchOff; // in this segment uint16_t targetOff; // in target segment uint8_t targetSeg; // 1-based target seg num (within this OMF) uint8_t byteCnt; // 3 = IMM24 uint8_t bitShift; // 0 }; // cINTERSEG opcode wire size: 0xF6 + ByteCnt + BitShift + OffsetPatch // + SegNum + OffsetReference = 1 + 1 + 1 + 2 + 1 + 2 = 8 bytes per site. static constexpr uint32_t OMF_CINTERSEG_BYTES_PER_SITE = 8; static std::vector readFile(const std::string &path) { std::ifstream f(path, std::ios::binary); if (!f) die("cannot open '" + path + "' for reading"); return std::vector((std::istreambuf_iterator(f)), std::istreambuf_iterator()); } static std::map readMap(const std::string &path) { std::map syms; std::ifstream f(path); if (!f) die("cannot open '" + path + "' for reading"); std::string line; while (std::getline(f, line)) { auto eq = line.find(" = "); if (eq == std::string::npos) continue; std::string name = line.substr(0, eq); std::string addr = line.substr(eq + 3); // Trim trailing whitespace. while (!name.empty() && std::isspace((unsigned char)name.back())) name.pop_back(); while (!addr.empty() && std::isspace((unsigned char)addr.back())) addr.pop_back(); try { syms[name] = std::stoul(addr, nullptr, 16); } catch (...) { /* skip non-hex entries */ } } return syms; } // Emit little-endian. static void put32(std::vector &v, uint32_t x) { v.push_back(x & 0xFF); v.push_back((x >> 8) & 0xFF); v.push_back((x >> 16) & 0xFF); v.push_back((x >> 24) & 0xFF); } static void put16(std::vector &v, uint16_t x) { v.push_back(x & 0xFF); v.push_back((x >> 8) & 0xFF); } // Emit one OMF segment record. Caller composes multiple records // back-to-back to form a multi-segment OMF file. // // `org` : absolute load address. 0 means "loader picks" (single- // segment mode). Non-zero (typical for multi-segment) // requests STATIC ABSBANK placement at that exact address. // `segNum` : 1-based segment number. // `entryOff`: offset within this segment to the program entry point; // only meaningful for the entry segment (typically 1), // ignored otherwise. // `kind` : OMF KIND field. Caller picks; v1 uses 0x8800 (STATIC | // ABSBANK | CODE) for multi-segment static placement, or // 0x0000 (CODE, dynamic) for single-segment legacy mode. static std::vector emitOneSeg(const std::vector &image, uint32_t entryOff, uint32_t org, uint16_t segNum, uint16_t kind, const std::string &name, uint32_t bssSize, uint32_t bssGap, const std::vector &relocSites, const std::vector &interSites); // Adapter: legacy single-seg call sites that read from `gReloc24Sites`. static std::vector emitOneSeg(const std::vector &image, uint32_t entryOff, uint32_t org, uint16_t segNum, uint16_t kind, const std::string &name, uint32_t bssSize = 0, uint32_t bssGap = 0) { static const std::vector noInter; return emitOneSeg(image, entryOff, org, segNum, kind, name, bssSize, bssGap, gReloc24Sites, noInter); } static std::vector emitOneSeg(const std::vector &image, uint32_t entryOff, uint32_t org, uint16_t segNum, uint16_t kind, const std::string &name, uint32_t bssSize, uint32_t bssGap, const std::vector &relocSites, const std::vector &interSites) { std::vector body; // Combined image: caller's LCONST data + zero-padding to bss-start // offset + bssSize zero bytes. We embed BSS-as-zeros in the LCONST // rather than relying on RESSPC zero-fill, because the GS/OS Loader's // ExpressLoad fast path doesn't reliably honor RESSPC for KIND=CODE // segments — writes past image.size() (= into RESSPC) were silently // lost (they went to GS/OS-owned memory). Embedding BSS as zeros // forces the Loader to allocate enough memory and read it from the // file. Caller passes bssGap = bytes of padding between LCONST and // BSS so BSS lands at the link-layout address (link816 page-aligns // bss-start up from rodata-end). std::vector combined; combined.reserve(image.size() + bssGap + bssSize); combined.insert(combined.end(), image.begin(), image.end()); combined.insert(combined.end(), bssGap, 0); combined.insert(combined.end(), bssSize, 0); if (!combined.empty()) { // LCONST opcode 0xF2: takes a NUMLEN-byte count followed by N // literal bytes. With NUMLEN=4 (standard for v2.1), the count // field is 4 bytes. Verified empirically against real /SYSTEM/ // START on GS/OS 6.0.2: every segment uses 0xF2 + 4-byte count. body.push_back(OMF_OP_LCONST); // LCONST opcode put32(body, static_cast(combined.size())); body.insert(body.end(), combined.begin(), combined.end()); } // cRELOC opcodes (0xF5): one per IMM24 reloc site. Format per // Merlin32's BuildOMFFile: // 1B opcode (0xF5) // 1B ByteCnt (3 for IMM24) // 1B BitShift (0 = no shift) // 2B OffsetPatch (offset in segment to patch) // 2B OffsetReference (in-segment offset of target) // The Loader rewrites segment[OffsetPatch..OffsetPatch+2] to be // (segPlacedBase + OffsetReference) at load time. This is what // makes JSL/JML/STAlong/etc. with intra-segment targets work when // the Loader places us at non-zero bank. for (const auto &s : relocSites) { body.push_back(OMF_OP_CRELOC); body.push_back(s.byteCnt); // ByteCnt (2 or 3) body.push_back(s.bitShift); // BitShift (0 or 16) put16(body, s.patchOff); // OffsetPatch put16(body, s.offsetRef); // OffsetReference } // cINTERSEG opcodes (0xF6) for cross-segment patch sites. Format // per Apple GS/OS Toolbox Reference Vol 3, Appendix B: // 1B 0xF6 opcode // 1B ByteCnt (3 for IMM24 — the only width usable here) // 1B BitShift (0) // 2B OffsetPatch (in this segment) // 1B SegNum (target seg number; cINTERSEG is single-file) // 2B OffsetReference (in target seg) // The Loader patches segment[OffsetPatch..OffsetPatch+ByteCnt-1] to // (targetSegPlacedBase + OffsetReference) ADDED to the constant // already there. link816 zeroes the patch bytes so the ADD becomes // the bare resolved address — see applyTextReloc inter branch. for (const auto &s : interSites) { body.push_back(OMF_OP_CINTERSEG); body.push_back(s.byteCnt); body.push_back(s.bitShift); put16(body, s.patchOff); body.push_back(s.targetSeg); put16(body, s.targetOff); } body.push_back(OMF_OP_END); // END opcode // Real OMF format (Merlin32 convention, verified GS/OS Loader-launchable): // - LABLEN = 10: both LOAD_NAME and SEG_NAME are 10 bytes wide, // space-padded. This is what Merlin32 emits and what GS/OS // Loader accepts when launching from Finder. Length-prefixed // names (LABLEN=0, what /SYSTEM/START FINDER and TOOL.SETUP // use) is documented in the OMF spec but NOT accepted by the // Loader for app launch — empirical finding: switching from // LABLEN=0 to LABLEN=10 was the key change that took our hello // from "OMF loaded but entry never JSL'd → $005C error" to // "marker $0078 = $42 set, code ran". constexpr uint8_t LABLEN_VAL = 10; std::vector loadName(10, 0x20); // 10 spaces std::string segNameTxt = name.substr(0, 10); // truncate to LABLEN std::vector segName(LABLEN_VAL, 0x20); // 10-byte field, space-padded for (size_t i = 0; i < segNameTxt.size(); i++) segName[i] = (uint8_t)segNameTxt[i]; constexpr uint16_t DISPNAME = 44; const uint16_t DISPDATA = static_cast( DISPNAME + loadName.size() + segName.size()); // LENGTH = in-memory segment size = LCONST data size (BSS already // appended as zeros to `combined` above). RESSPC = 0 because the // BSS bytes are part of LCONST — the Loader's normal LCONST processing // allocates and fills the memory. Tried RESSPC > 0 first, but the // ExpressLoad fast path doesn't honor RESSPC for CODE-KIND segments. const uint32_t LENGTH = static_cast(combined.size()); const uint32_t BYTECNT = DISPDATA + static_cast(body.size()); const uint32_t RESSPC = 0; // BANKSIZE = 0x10000 — segment fits in one 64KB bank. // Earlier I tried 0 (matched one decoded file) but real // executable code segments use 0x10000. const uint32_t BANKSIZE = 0x10000; // 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; put32(hdr, BYTECNT); put32(hdr, RESSPC); put32(hdr, LENGTH); hdr.push_back(0x00); // undefined hdr.push_back(LABLEN_VAL); // LABLEN (10 = fixed-width names) hdr.push_back(4); // NUMLEN hdr.push_back(0x02); // VERSION (0x02 = OMF v2.1; 0x01 = v2.0) // Earlier we used 0x21 here thinking it was BCD-encoded "2.1" — // it's not. The VERSION byte uses an enum: 0x00=v1.0, 0x01=v2.0, // 0x02=v2.1. Real GS/OS apps decoded from a system disk have // 0x02 here. GS/OS Loader rejects 0x21 with error $1102 because // there's no version with that code. put32(hdr, BANKSIZE); put16(hdr, kind); hdr.push_back(0x00); hdr.push_back(0x00); // undefined (2 bytes) put32(hdr, org); put32(hdr, ALIGN); hdr.push_back(NUMSEX); hdr.push_back(0x00); // undefined put16(hdr, segNum); put32(hdr, entryOff); put16(hdr, DISPNAME); put16(hdr, DISPDATA); if (hdr.size() != 44) die("internal: header size != 44"); std::vector out; out.insert(out.end(), hdr.begin(), hdr.end()); out.insert(out.end(), loadName.begin(), loadName.end()); out.insert(out.end(), segName.begin(), segName.end()); out.insert(out.end(), body.begin(), body.end()); return out; } // Emit a "~Direct" DP/Stack segment. When the GS/OS System Loader // encounters this segment kind (KIND low-5 = 0x12), it calls Memory // Manager NewHandle to allocate `length` bytes of page-aligned, locked // memory in bank $00, then sets the application's DP and SP to point // into that block. Without an explicit DP/Stack segment in the OMF, // the Loader allocates a default 4KB chunk — usually enough, but // declaring our own size makes intent explicit and lets us bump it // without runtime fiddling. // // Source: Apple IIgs GS/OS Reference Vol 1 (System Loader chapter): // "You define your program's stack and direct-page needs by // specifying a 'direct-page/stack' object segment (KIND = $12). // The size of the segment is the total amount of stack and // direct-page space your program needs. When the System Loader // finds this segment at load time, it calls the Memory Manager to // allocate a page-aligned, locked memory block of that size in // bank $00." // // The body is an LCONST opcode followed by `length` zero bytes plus an // END opcode — matching the real-world format used by every GNO/ME // command (e.g. /GNO.BOOT/bin/echo's ~_STACK seg). Empirically a body // of just END (no LCONST, relying on RESSPC for allocation) makes the // GS/OS Loader's ExpressLoad fast path silently drop the seg and fall // back to its default 4 KB DP/Stack — hence this code emits real // content so the Loader has something to copy. KIND = 0x4012 (RELOAD // | DP/Stack) also matches the working GNO format; the earlier 0x1012 // (PRIVATE | DP/Stack) is what `makedirect` ships but doesn't survive // ExpressLoad fast-path processing. static std::vector emitDpStackSeg(uint32_t length, uint16_t segNum) { std::vector body; body.push_back(0xF2); // LCONST opcode put32(body, length); // 4-byte literal length body.insert(body.end(), length, 0); // `length` zero bytes body.push_back(0x00); // END opcode constexpr uint8_t LABLEN_VAL = 10; const std::string segNameTxt = "~Direct"; std::vector loadName(LABLEN_VAL, 0x20); std::vector segName(LABLEN_VAL, 0x20); for (size_t i = 0; i < segNameTxt.size(); i++) segName[i] = (uint8_t)segNameTxt[i]; constexpr uint16_t DISPNAME = 44; const uint16_t DISPDATA = static_cast( DISPNAME + loadName.size() + segName.size()); const uint32_t LENGTH = length; // memory size requested const uint32_t BYTECNT = DISPDATA + static_cast(body.size()); // RESSPC = 0 because the bytes are carried in LCONST (matches the // bss-as-zeros approach used for the user CODE seg — the Loader's // ExpressLoad fast path can't be trusted to honor RESSPC). const uint32_t RESSPC = 0; const uint32_t BANKSIZE = 0; // DP/Stack lives in bank 0 const uint32_t ALIGN = 0x100; // page-aligned per spec const uint16_t KIND = OMF_KIND_DPSTACK; // DP/Stack | RELOAD std::vector hdr; put32(hdr, BYTECNT); put32(hdr, RESSPC); put32(hdr, LENGTH); hdr.push_back(0x00); // undefined hdr.push_back(LABLEN_VAL); // LABLEN hdr.push_back(4); // NUMLEN hdr.push_back(0x02); // VERSION (v2.1) put32(hdr, BANKSIZE); put16(hdr, KIND); hdr.push_back(0x00); hdr.push_back(0x00); // undefined put32(hdr, /*ORG*/0); put32(hdr, ALIGN); hdr.push_back(/*NUMSEX*/0); hdr.push_back(0x00); put16(hdr, segNum); put32(hdr, /*ENTRY*/0); put16(hdr, DISPNAME); put16(hdr, DISPDATA); if (hdr.size() != 44) die("internal: DP/Stack hdr size != 44"); std::vector out; out.insert(out.end(), hdr.begin(), hdr.end()); out.insert(out.end(), loadName.begin(), loadName.end()); out.insert(out.end(), segName.begin(), segName.end()); out.insert(out.end(), body.begin(), body.end()); return out; } // Legacy single-segment wrapper. // // KIND=0x1000 (CODE | PRIV). This is what Merlin32 emits for single- // segment GS/OS apps and what GS/OS Loader actually launches via // Finder double-click. KIND=0x8000 (CODE|STATIC) was earlier hypothesis // based on extracting ABOUT from real FINDER, but ABOUT is a sub- // segment of FINDER, not a standalone app — so its KIND isn't a valid // model. PRIV bit signals "loaded with the rest of the app" and is the // reliable choice empirically validated by Merlin32-built hello.s16 // running successfully under MAME-Lua-driven Finder launch. // // `stackSize` > 0 appends a ~Direct DP/Stack segment of that size as // segment 2. 0 = caller doesn't want one (Loader uses its 4KB // default). static std::vector emitOMF(const std::vector &image, uint32_t entryOffset, const std::string &name, uint32_t stackSize = 0, uint32_t bssSize = 0, uint32_t bssGap = 0) { if (stackSize == 0) { return emitOneSeg(image, entryOffset, /*org*/0, /*segNum*/1, /*kind*/OMF_KIND_CODE_PRIV, name, bssSize, bssGap); } // DP/Stack segment ordering: Apple's `makedirect` reference utility // assigns the DP/Stack as SEGNUM 1 (its own object); when linked // into a multi-segment OMF, ordering matters because the Loader // walks segments in file order. We put the DP/Stack FIRST so the // Loader allocates the chunk before reading the code segment, then // sets DP and SP appropriately when entering our code. auto dpSeg = emitDpStackSeg(stackSize, /*segNum*/1); auto codeSeg = emitOneSeg(image, entryOffset, /*org*/0, /*segNum*/2, /*kind*/OMF_KIND_CODE_PRIV, name, bssSize, bssGap); std::vector out; out.insert(out.end(), dpSeg.begin(), dpSeg.end()); out.insert(out.end(), codeSeg.begin(), codeSeg.end()); return out; } // Emit an ExpressLoad-able OMF wrapping a single user segment. This is // what real GS/OS apps look like: a `~ExpressLoad` segment as seg 1, // then the actual code as seg 2. // // Why we need ExpressLoad: replacing /SYSTEM/START with a single- // segment OMF (no ExpressLoad) makes the GS/OS Loader place our // segment in RAM but never JSL the entry — verified by writing a // marker as the first instruction of crt0Gsos and observing the // marker remained 0 across the entire boot. // // ExpressLoad format reverse-engineered from real /SYSTEM/START // (FINDER) on GS/OS 6.0.2 disk. Each ExpressLoad-able file's seg 1 // is a `~ExpressLoad` data segment containing a load script. // // The load script (stored as the LCONST data of the ExpressLoad seg): // +0..1 word file_ref = 0 // +2..3 word reserved = 0 // +4..5 word extra = 0 (Neil Parker's docs omit this) // +6..7 word count = N - 2 where N = total segs // +8.. 8B/seg segment list = (N - 1) entries: // +0..1: self-rel offset to header info entry // +2..3: flags = 0 // +4..7: handle = 0 // +Var 2B/seg remap list = (N - 1) words: // new segment number for old position // +Var Var/seg header info entries: // +0..3: data offset in file (= body op + 5) // +4..7: data length (= seg LENGTH field) // +8..11: reloc offset in file (0 if no relocs) // +12..15: reloc length (0 if no relocs) // +16..47: header copy bytes [12..43] of the // user segment, with DISPDATA zeroed // +48..57: LOAD_NAME (10 bytes) // +58.. : SEG_NAME (length-prefixed) // // All counts use NUMLEN=4 (4-byte length on LCONST opcodes). // Phase C — multi-user-segment ExpressLoad descriptor. Each entry // describes one user code segment (plus its optional BSS tail and // per-segment cRELOC sites recorded by link816). ENTRY_OFFSET is only // meaningful on the first user seg (= seg 2 of the OMF; the Loader // JSLs there to start the program). bssSize/bssGap follow the same // LCONST-embedded-zeros convention as emitOneSeg. relocSites is the // intra-segment cRELOC list — cross-segment references (cINTERSEG) // are NOT yet emitted; this descriptor is the prep for that work. struct UserSeg { std::vector image; uint32_t entryOffset = 0; std::string name; uint32_t bssSize = 0; uint32_t bssGap = 0; std::vector relocSites; std::vector interSites; // Phase C.2 cINTERSEG sites }; static std::vector emitOmfExpressLoad( const std::vector &users, uint32_t stackSize = 0); // Legacy single-user-seg adapter. Reads cRELOC sites from the global // `gReloc24Sites` (filled by --relocs). static std::vector emitOmfExpressLoad( const std::vector &image, uint32_t entryOffset, const std::string &userSegName, uint32_t stackSize = 0, uint32_t bssSize = 0, uint32_t bssGap = 0) { UserSeg u; u.image = image; u.entryOffset = entryOffset; u.name = userSegName; u.bssSize = bssSize; u.bssGap = bssGap; u.relocSites = gReloc24Sites; return emitOmfExpressLoad(std::vector{u}, stackSize); } static std::vector emitOmfExpressLoad( const std::vector &users, uint32_t stackSize) { if (users.empty()) die("emitOmfExpressLoad: no user segments"); // Step 1: build every user segment using KIND=0x1000 (CODE|PRIV). // Same KIND emitOMF uses for single-segment apps. Each user seg // gets sequential SEGNUM starting at 2 (seg 1 is ~ExpressLoad). // The optional DP/Stack seg gets the trailing SEGNUM. std::vector> userSegs; 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*/segKind, u.name, u.bssSize, u.bssGap, u.relocSites, u.interSites)); } // Optionally build the DP/Stack segment. If present it lives in the // file AFTER the user seg and gets its own ExpressLoad segtable + // remap + header_info entries — otherwise the Loader's ExpressLoad // fast path never sees the KIND=0x4012 record and reverts to its // default 4KB DP/Stack allocation (silent --stack-size no-op). const bool haveDpStack = (stackSize != 0); std::vector dpStackSeg; if (haveDpStack) { dpStackSeg = emitDpStackSeg(stackSize, /*segNum*/3); } // Step 2: figure out the file offsets we'll need to bake into the // load script. We don't know the ExpressLoad segment's total size // yet — but we can compute it because each component is a fixed // function of the user segment name length. // // ExpressLoad LCONST data layout (matches Merlin32 source — see // BuildExpressLoadSegment in Merlin32's a65816_OMF.c): // 6 bytes header (4-byte reserved DWORD + 2-byte count WORD) // 8 bytes/seg segment list (1 entry per non-ExpressLoad segment) // 2 bytes/seg remap list (1 entry per non-ExpressLoad segment) // 68 bytes/seg header_info (16B offsets + 32B hdr copy + 10B LOAD_NAME + 10B SEG_NAME) // total: 6 + 78*N bytes for N non-ExpressLoad segs // // KEY FIX from earlier emitter version: header is 6 bytes, NOT 8. // I had written 8 bytes (file_ref WORD + reserved WORD + extra WORD + // count WORD) based on misreading /SYSTEM/START's bytes. Merlin32 // uses (reserved DWORD + count WORD) = 6 bytes total. /SYSTEM/START // has count=0 in the 6-byte interpretation which means it uses some // other variant (maybe APW Express's older format), but Merlin32's // format is what we know is GS/OS-loader-accepted today. constexpr uint32_t HDR_SIZE = 44; constexpr uint32_t LOAD_NAME_SIZE = 10; constexpr uint32_t SEG_NAME_SIZE = 10; // LABLEN=10 → fixed-width SEG_NAME constexpr uint32_t SEGTAB_ENTRY = 8; constexpr uint32_t REMAP_ENTRY = 2; constexpr uint32_t HDR_INFO_ENTRY = 16 + 32 + LOAD_NAME_SIZE + SEG_NAME_SIZE; // 68 constexpr uint32_t HEADER_BYTES = 6; const uint32_t userNameAreaSize = LOAD_NAME_SIZE + SEG_NAME_SIZE; // ExpressLoad's own segment metrics. The name "~ExpressLoad" is 12 // chars and won't fit in a LABLEN=10 field, so the ExpressLoad seg // uses LABLEN=0 (length-prefixed name): 1 length byte + 12 chars. const std::string elName = "~ExpressLoad"; const uint32_t elNameAreaSize = LOAD_NAME_SIZE + 1 + (uint32_t)elName.size(); // Total non-ExpressLoad segs = N user segs + optional DP/Stack. // Rebuild dpStackSeg with the correct trailing segNum now that we // know how many user segs there are. if (haveDpStack) { dpStackSeg = emitDpStackSeg(stackSize, /*segNum*/(uint16_t)(users.size() + 2)); } const uint32_t nSegs = (uint32_t)users.size() + (haveDpStack ? 1u : 0u); const uint32_t elDataSize = HEADER_BYTES + (SEGTAB_ENTRY + REMAP_ENTRY + HDR_INFO_ENTRY) * nSegs; // Body size = 1 byte LCONST opcode + 4 byte length + data + 1 byte END const uint32_t elBodySize = 1 + 4 + elDataSize + 1; const uint32_t elSegSize = HDR_SIZE + elNameAreaSize + elBodySize; // User segment file offsets (cascade after ExpressLoad seg). For // each user seg, dataOff = (segStart + HDR_SIZE + nameArea + 1B // LCONST opcode + 4B length). std::vector userDataOffs; std::vector userCRelocOffs; std::vector userSegStarts; userDataOffs.reserve(users.size()); userCRelocOffs.reserve(users.size()); userSegStarts.reserve(users.size()); uint32_t segStart = elSegSize; for (size_t k = 0; k < users.size(); k++) { userSegStarts.push_back(segStart); const uint32_t bodyOpOff = segStart + HDR_SIZE + userNameAreaSize; const uint32_t dataOff = bodyOpOff + 5; userDataOffs.push_back(dataOff); const uint32_t dataLen = (uint32_t)users[k].image.size() + users[k].bssGap + users[k].bssSize; userCRelocOffs.push_back(dataOff + dataLen); segStart += (uint32_t)userSegs[k].size(); } // DP/Stack segment file offsets (after all user segs). The DP/Stack // body mirrors the real GNO/ME ~_STACK seg format: an LCONST opcode // + 4 byte length + `stackSize` zero bytes + END. ExpressLoad's // hdr_info entry has to point at the LCONST data so the Loader // copies the right number of zeros into the allocated chunk — a // body of just END (RESSPC-only) silently no-ops on the // ExpressLoad fast path, which is the bug this whole section fixes. const uint32_t dpStackSegStart = segStart; const uint32_t dpStackBodyOff = dpStackSegStart + HDR_SIZE + (LOAD_NAME_SIZE + SEG_NAME_SIZE); const uint32_t dpStackDataOff = dpStackBodyOff + 5; // 1 op + 4 length // Step 3: build the ExpressLoad LCONST data. std::vector elData; // Header (6 bytes): reserved DWORD + count WORD. count = N-2 where // 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 // Segment list: one 8-byte entry per non-ExpressLoad segment. Each // entry's first WORD is the SELF-RELATIVE offset (from this entry's // own start) to the segment's header_info record. const uint32_t segTableOff = HEADER_BYTES; const uint32_t remapOff = segTableOff + SEGTAB_ENTRY * nSegs; const uint32_t hdrInfoOff = remapOff + REMAP_ENTRY * nSegs; for (uint32_t i = 0; i < nSegs; i++) { const uint32_t thisEntryOff = segTableOff + SEGTAB_ENTRY * i; const uint32_t thisHdrInfoOff = hdrInfoOff + HDR_INFO_ENTRY * i; put16(elData, (uint16_t)(thisHdrInfoOff - thisEntryOff)); // self-rel put16(elData, 0); // flags put32(elData, 0); // handle } // Remap list: 1 WORD per non-ExpressLoad seg, giving the new // segment number for each old segment position. Each non-EL seg // gets new SEGNUM = (positionInExpressLoad + 2), starting at 2 for // the first user seg. DP/Stack lands last. for (uint32_t i = 0; i < nSegs; i++) { put16(elData, (uint16_t)(i + 2)); } // Header info entries — one per non-ExpressLoad segment. Each // entry is 68 bytes: 16B (dataOff,dataLen,relocOff,relocLen) + 32B // header copy + 10B LOAD_NAME + 10B SEG_NAME. data length = LCONST // data size in the file. emitOneSeg embeds bssGap bytes of zero // padding + bssSize bytes of BSS-as-zeros in the LCONST after the // caller's image, so the on-disk data is image.size() + bssGap + // bssSize bytes. cRELOC opcodes (if any) are emitted by emitOneSeg // directly after the LCONST data and before the END opcode; tell // ExpressLoad where they live so the Loader can apply them. for (size_t k = 0; k < users.size(); k++) { const auto &u = users[k]; const auto &seg = userSegs[k]; if (seg.size() < HDR_SIZE) die("internal: user seg too small"); const uint32_t dataLen = (uint32_t)u.image.size() + u.bssGap + u.bssSize; put32(elData, userDataOffs[k]); // data offset in file put32(elData, dataLen); // data length const uint32_t relocLen = OMF_CRELOC_BYTES_PER_SITE * (uint32_t)u.relocSites.size() + OMF_CINTERSEG_BYTES_PER_SITE * (uint32_t)u.interSites.size(); if (relocLen == 0) { put32(elData, 0); // reloc offset put32(elData, 0); // reloc length } else { put32(elData, userCRelocOffs[k]); put32(elData, relocLen); } // Header copy: bytes [12..43] of user segment header, DISPDATA → 0. elData.insert(elData.end(), seg.begin() + 12, seg.begin() + HDR_SIZE); // DISPDATA is at offset 42..43 of the original header; in the copy // (which omits the first 12 bytes), it lands at offset 30..31. elData[elData.size() - 32 + 30] = 0; elData[elData.size() - 32 + 31] = 0; // LOAD_NAME (10 bytes, space-padded — matches Merlin convention) for (int i = 0; i < (int)LOAD_NAME_SIZE; i++) elData.push_back(0x20); // SEG_NAME (10 bytes fixed-width, space-padded) std::string truncated = u.name.substr(0, SEG_NAME_SIZE); for (size_t i = 0; i < SEG_NAME_SIZE; i++) { elData.push_back(i < truncated.size() ? (uint8_t)truncated[i] : 0x20); } } // Header info entry for the DP/Stack segment (when present). // data_off / data_len point at the LCONST zero bytes carried in the // DP/Stack seg's body, mirroring the working real-world layout // (GNO/ME ~_STACK). No cRELOC entries for a DP/Stack seg, so // reloc fields are 0. if (haveDpStack) { if (dpStackSeg.size() < HDR_SIZE) die("internal: DP/Stack seg too small"); put32(elData, dpStackDataOff); // data offset (LCONST data) put32(elData, stackSize); // data length (= stack size) put32(elData, 0); // reloc offset put32(elData, 0); // reloc length // Header copy: bytes [12..43] of DP/Stack segment header. elData.insert(elData.end(), dpStackSeg.begin() + 12, dpStackSeg.begin() + HDR_SIZE); elData[elData.size() - 32 + 30] = 0; // DISPDATA hi → 0 elData[elData.size() - 32 + 31] = 0; // LOAD_NAME (10 bytes, space-padded) for (int i = 0; i < (int)LOAD_NAME_SIZE; i++) elData.push_back(0x20); // SEG_NAME = "~Direct" padded to 10 bytes (must match the value // stored by emitDpStackSeg, otherwise ExpressLoad's name match // could fail; the seg-name area in the file uses 10 spaces base // with "~Direct" overwriting the first 7). const char *dpName = "~Direct"; const size_t dpNameLen = 7; for (size_t i = 0; i < SEG_NAME_SIZE; i++) { elData.push_back(i < dpNameLen ? (uint8_t)dpName[i] : 0x20); } } if (elData.size() != elDataSize) die("internal: ExpressLoad data size mismatch"); // Step 4: build the ExpressLoad segment header. // KIND=0x8001 (DATA|STATIC), BANKSIZE=0 (DATA segs use 0, not 0x10000). std::vector elHdr; const uint32_t elBytecnt = HDR_SIZE + elNameAreaSize + elBodySize; put32(elHdr, elBytecnt); // BYTECNT put32(elHdr, 0); // RESSPC put32(elHdr, elDataSize); // LENGTH (= LCONST data size) elHdr.push_back(0); // undef elHdr.push_back(0); // LABLEN elHdr.push_back(4); // NUMLEN elHdr.push_back(2); // VERSION (0x02 = v2.1) put32(elHdr, 0); // BANKSIZE = 0 for DATA seg put16(elHdr, OMF_KIND_DATA_STATIC); // KIND = DATA|STATIC elHdr.push_back(0); elHdr.push_back(0); // undef put32(elHdr, 0); // ORG put32(elHdr, 0); // ALIGN elHdr.push_back(0); // NUMSEX elHdr.push_back(0); // undef put16(elHdr, 1); // SEGNUM = 1 put32(elHdr, 0); // ENTRY = 0 put16(elHdr, (uint16_t)HDR_SIZE); // DISPNAME = 44 put16(elHdr, (uint16_t)(HDR_SIZE + elNameAreaSize)); // DISPDATA if (elHdr.size() != HDR_SIZE) die("internal: el hdr size != 44"); // Step 5: assemble the ExpressLoad segment. std::vector elSeg; elSeg.insert(elSeg.end(), elHdr.begin(), elHdr.end()); for (int i = 0; i < (int)LOAD_NAME_SIZE; i++) elSeg.push_back(0); elSeg.push_back((uint8_t)elName.size()); for (char c : elName) elSeg.push_back((uint8_t)c); // Body: LCONST opcode + 4-byte length + data + END elSeg.push_back(0xF2); put32(elSeg, elDataSize); elSeg.insert(elSeg.end(), elData.begin(), elData.end()); elSeg.push_back(0x00); if (elSeg.size() != elSegSize) die("internal: ExpressLoad segment size mismatch"); // Step 6: concatenate ExpressLoad + user segments + optional DP/Stack. // The DP/Stack seg's presence is now also recorded in the // ExpressLoad load script (segtable + remap + header_info entries // above) so the Loader's fast path honors KIND=0x4012 instead of // silently dropping it to its default 4 KB DP/Stack allocation. std::vector result; result.insert(result.end(), elSeg.begin(), elSeg.end()); for (const auto &seg : userSegs) result.insert(result.end(), seg.begin(), seg.end()); if (haveDpStack) { result.insert(result.end(), dpStackSeg.begin(), dpStackSeg.end()); } return result; } // Bare-bones manifest parser. link816's manifest is structured as // `{ "segments": [ { "num": N, "base": "0xHHHHHH", "size": N, // "image": "PATH", "entry_offset": "0xHHHH" }, ... ] }` with strict // formatting (one field per line, no nested whitespace tricks). We // match each field with simple regex/find — good enough since we're // the only producer of this format. struct ManifestSeg { uint32_t num = 0; uint32_t base = 0; uint32_t entryOff = 0; std::string image; std::string name; }; static std::string extractStringField(const std::string &block, const std::string &key) { std::string needle = "\"" + key + "\":"; size_t p = block.find(needle); if (p == std::string::npos) return {}; // Skip whitespace after the colon. If the next non-space char // isn't a quote, the value is a bare number — return empty so // the caller falls through to the bare-number path (without // accidentally consuming the next field's quoted string). p += needle.size(); while (p < block.size() && std::isspace((unsigned char)block[p])) p++; if (p >= block.size() || block[p] != '"') return {}; size_t e = block.find('"', p + 1); if (e == std::string::npos) return {}; return block.substr(p + 1, e - p - 1); } static uint32_t extractNumberField(const std::string &block, const std::string &key) { // Number can appear bare (size: 1234) or as a hex string ("0x..."). std::string s = extractStringField(block, key); if (!s.empty()) { return static_cast(std::stoul(s, nullptr, 0)); } std::string needle = "\"" + key + "\":"; size_t p = block.find(needle); if (p == std::string::npos) return 0; p += needle.size(); while (p < block.size() && std::isspace((unsigned char)block[p])) p++; size_t e = p; while (e < block.size() && (std::isdigit((unsigned char)block[e]) || block[e] == 'x' || block[e] == 'X' || (block[e] >= 'a' && block[e] <= 'f') || (block[e] >= 'A' && block[e] <= 'F'))) e++; if (e == p) return 0; return static_cast(std::stoul(block.substr(p, e - p), nullptr, 0)); } static std::vector parseManifest(const std::string &path) { std::ifstream f(path); if (!f) die("cannot open '" + path + "' for reading"); std::string text((std::istreambuf_iterator(f)), std::istreambuf_iterator()); std::vector segs; // Find "segments": [ ... ] then split into per-segment {} blocks. size_t arrStart = text.find("\"segments\""); if (arrStart == std::string::npos) die("manifest missing 'segments'"); arrStart = text.find('[', arrStart); if (arrStart == std::string::npos) die("manifest 'segments' not array"); size_t pos = arrStart + 1; while (pos < text.size()) { size_t obStart = text.find('{', pos); if (obStart == std::string::npos) break; // Match closing } via brace depth. int depth = 1; size_t obEnd = obStart + 1; while (obEnd < text.size() && depth > 0) { if (text[obEnd] == '{') depth++; else if (text[obEnd] == '}') depth--; if (depth > 0) obEnd++; } if (depth != 0) die("manifest segment block unterminated"); std::string block = text.substr(obStart, obEnd - obStart + 1); ManifestSeg seg; seg.num = extractNumberField(block, "num"); seg.base = extractNumberField(block, "base"); seg.entryOff = extractNumberField(block, "entry_offset"); seg.image = extractStringField(block, "image"); seg.name = extractStringField(block, "name"); if (seg.image.empty()) die("manifest segment missing 'image'"); if (seg.name.empty()) seg.name = "SEG" + std::to_string(seg.num); segs.push_back(std::move(seg)); pos = obEnd + 1; size_t closing = text.find_first_not_of(" \t\n\r,", pos); if (closing != std::string::npos && text[closing] == ']') break; } if (segs.empty()) die("manifest has no segments"); return segs; } 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 + "'"); if (v > 0xFFFFFF) die("address '" + s + "' exceeds 24-bit range"); return static_cast(v); } static void usage(const char *argv0) { std::fprintf(stderr, "usage: %s --input FLAT --map FILE --base ADDR --entry SYM\n" " --output OMF [--name NAME] [--expressload]\n" " [--relocs FILE] [--stack-size BYTES]\n" " %s --manifest MFEST --output OMF\n" "\n" " --expressload emit ExpressLoad-able OMF (required for boot\n" " launchers under real GS/OS Loader).\n" " --relocs FILE read IMM24 reloc list from link816's --reloc-out\n" " sidecar; emit cRELOC (0xF5) opcodes after LCONST\n" " so the Loader patches intra-segment 24-bit refs\n" " (JSL/JML/STAlong/etc.) when placing the segment.\n" " --stack-size N append a ~Direct DP/Stack segment (KIND=0x4012)\n" " of N bytes. The Loader allocates a page-aligned\n" " block of this size in bank 0 for combined DP +\n" " stack use. N must be page-multiple (>= 256).\n" " Default 0 (Loader uses its built-in 4KB default).\n" " Implicitly enables --expressload (the GS/OS\n" " Loader's slow path rejects multi-seg OMFs).\n" " Not yet supported with --manifest.\n", argv0, argv0); std::exit(2); } } // namespace int main(int argc, char **argv) { std::string input, mapFile, output, entry = "main", name, manifest; std::string relocFile; uint32_t base = 0; bool baseSet = false; bool expressload = false; uint32_t stackSize = 0; int i = 1; while (i < argc) { std::string a = argv[i]; if (a == "--input") { if (++i >= argc) usage(argv[0]); input = argv[i++]; } else if (a == "--map") { if (++i >= argc) usage(argv[0]); mapFile = argv[i++]; } else if (a == "--base") { if (++i >= argc) usage(argv[0]); base = parseInt(argv[i++]); baseSet = true; } else if (a == "--entry") { if (++i >= argc) usage(argv[0]); entry = argv[i++]; } else if (a == "--name") { if (++i >= argc) usage(argv[0]); name = argv[i++]; } else if (a == "--manifest") { if (++i >= argc) usage(argv[0]); manifest = argv[i++]; } else if (a == "--output" || a == "-o") { if (++i >= argc) usage(argv[0]); output = argv[i++]; } else if (a == "--expressload") { expressload = true; i++; } else if (a == "--relocs") { if (++i >= argc) usage(argv[0]); relocFile = argv[i++]; } else if (a == "--stack-size") { if (++i >= argc) usage(argv[0]); stackSize = parseInt(argv[i++]); } else if (a == "-h" || a == "--help") usage(argv[0]); else die("unknown option '" + a + "'"); } if (output.empty()) usage(argv[0]); if (stackSize != 0) { if (stackSize < 0x100) die("--stack-size must be at least 256 bytes (1 page)"); if (stackSize % 0x100 != 0) die("--stack-size must be a multiple of 256 (page-aligned)"); if (stackSize > 0xFFFF) die("--stack-size cannot exceed 65535 bytes (one bank)"); // --stack-size + --manifest is allowed when ExpressLoad wraps // the multi-seg file (auto-enabled below); the DP/Stack seg // gets the trailing SEGNUM after all user segs. // Plain (non-ExpressLoad) multi-segment OMFs do not launch // correctly under the GS/OS 6.0.2 Loader — verified empirically: // the bare DP/Stack + code combo is rejected (program never // executes), but ExpressLoad + DP/Stack works. Auto-enable // ExpressLoad whenever --stack-size is requested. expressload = true; } // Load reloc list, if provided. // Sidecar v3 layout: u32 count + 12 bytes per entry // { 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). // // 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) | ((uint32_t)raw[2] << 16) | ((uint32_t)raw[3] << 24); const size_t entrySize = 12; if (raw.size() != 4 + entrySize * cnt) die("--relocs file size mismatch: count=" + std::to_string(cnt) + " expected " + std::to_string(4 + entrySize*cnt) + " bytes, got " + std::to_string(raw.size())); gReloc24Sites.reserve(cnt); for (uint32_t k = 0; k < cnt; k++) { size_t off = 4 + k * entrySize; uint32_t patchOff = (uint32_t)raw[off] | ((uint32_t)raw[off+1] << 8) | ((uint32_t)raw[off+2] << 16) | ((uint32_t)raw[off+3] << 24); uint32_t offRef = (uint32_t)raw[off+4] | ((uint32_t)raw[off+5] << 8) | ((uint32_t)raw[off+6] << 16) | ((uint32_t)raw[off+7] << 24); uint8_t byteCnt = raw[off+8]; uint8_t bitShift = raw[off+9]; if (patchOff > 0xFFFF || offRef > 0xFFFF) die("reloc site out of 16-bit range — segment too large?"); if (byteCnt != 2 && byteCnt != 3) die("reloc site byteCnt=" + std::to_string(byteCnt) + " (must be 2 or 3)"); if (bitShift != 0 && bitShift != 16) die("reloc site bitShift=" + std::to_string(bitShift) + " (must be 0 or 16)"); RelocSite s; s.patchOff = (uint16_t)patchOff; s.offsetRef = (uint16_t)offRef; s.byteCnt = byteCnt; s.bitShift = bitShift; gReloc24Sites.push_back(s); } } // Multi-segment mode. if (!manifest.empty()) { auto segs = parseManifest(manifest); // --manifest + --expressload: wrap N user segments in a single // ~ExpressLoad descriptor (Phase C.1). Each user seg gets // KIND=0x1000 (CODE|PRIV), so the Loader picks banks dynamically // — programs MUST NOT depend on link-time bank placement. Both // intra-segment cRELOC and cross-segment IMM24 targets (via // cINTERSEG, 0xF6) are handled: Phase C.2 (per-seg sidecars with // intra + inter site lists) is implemented in the loop below. // Intra-only single-sidecar builds still use global gReloc24Sites. if (expressload) { std::vector users; users.reserve(segs.size()); // Phase C.2 — when link816 is invoked with // --multi-seg-expressload --reloc-out FILE, it writes per-seg // sidecars at FILE.seg.reloc with intra + inter site // lists. Look for them next to the user-supplied relocFile. for (size_t k = 0; k < segs.size(); ++k) { const auto &s = segs[k]; UserSeg u; u.image = readFile(s.image); u.entryOffset = (k == 0) ? s.entryOff : 0; u.name = s.name; if (!relocFile.empty()) { char perSegPath[512]; std::snprintf(perSegPath, sizeof(perSegPath), "%s.seg%u.reloc", relocFile.c_str(), s.num); std::ifstream pf(perSegPath, std::ios::binary); if (pf) { std::vector raw( (std::istreambuf_iterator(pf)), std::istreambuf_iterator()); if (raw.size() < 8) die(std::string(perSegPath) + ": too small"); uint32_t intraCount = (uint32_t)raw[0] | ((uint32_t)raw[1] << 8) | ((uint32_t)raw[2] << 16) | ((uint32_t)raw[3] << 24); uint32_t interCount = (uint32_t)raw[4] | ((uint32_t)raw[5] << 8) | ((uint32_t)raw[6] << 16) | ((uint32_t)raw[7] << 24); size_t want = 8 + 12 * (size_t)intraCount + 12 * (size_t)interCount; if (raw.size() != want) die(std::string(perSegPath) + ": size mismatch"); size_t off = 8; for (uint32_t i = 0; i < intraCount; i++) { uint32_t po = (uint32_t)raw[off] | ((uint32_t)raw[off+1] << 8) | ((uint32_t)raw[off+2] << 16) | ((uint32_t)raw[off+3] << 24); uint32_t orf = (uint32_t)raw[off+4] | ((uint32_t)raw[off+5] << 8) | ((uint32_t)raw[off+6] << 16) | ((uint32_t)raw[off+7] << 24); RelocSite r; r.patchOff = (uint16_t)po; r.offsetRef = (uint16_t)orf; r.byteCnt = raw[off+8]; r.bitShift = raw[off+9]; u.relocSites.push_back(r); off += 12; } for (uint32_t i = 0; i < interCount; i++) { uint32_t po = (uint32_t)raw[off] | ((uint32_t)raw[off+1] << 8) | ((uint32_t)raw[off+2] << 16) | ((uint32_t)raw[off+3] << 24); uint32_t to = (uint32_t)raw[off+4] | ((uint32_t)raw[off+5] << 8) | ((uint32_t)raw[off+6] << 16) | ((uint32_t)raw[off+7] << 24); InterRelocSite is; is.patchOff = (uint16_t)po; is.targetOff = (uint16_t)to; // Remap link816 seg N → OMF SEGNUM (N+1): // ExpressLoad occupies OMF seg 1, so each // user seg's OMF SEGNUM is link816 seg + 1. is.targetSeg = (uint8_t)(raw[off+8] + 1); is.byteCnt = raw[off+9]; is.bitShift = raw[off+10]; u.interSites.push_back(is); off += 12; } } } // Back-compat for layouts without per-seg sidecars: seg // 1 inherits the global --relocs flat list. if (u.relocSites.empty() && u.interSites.empty() && k == 0) u.relocSites = gReloc24Sites; users.push_back(std::move(u)); } auto blob = emitOmfExpressLoad(users, stackSize); std::ofstream f(output, std::ios::binary); if (!f) die("cannot open '" + output + "' for writing"); f.write(reinterpret_cast(blob.data()), blob.size()); std::fprintf(stderr, "OMF: ExpressLoad + %zu user segments -> %s (%zu bytes total)\n", segs.size(), output.c_str(), blob.size()); return 0; } std::vector blob; size_t totalPayload = 0; for (size_t k = 0; k < segs.size(); ++k) { const auto &s = segs[k]; auto img = readFile(s.image); // Multi-segment: STATIC | ABSBANK | CODE. STATIC tells // the loader not to relocate the segment (we baked all // intra-segment relocations at link time and have no // INTERSEG / RELOC opcodes); ABSBANK + ORG=base pins it // to a specific bank. CODE is the default (type 0). const uint16_t kind = OMF_KIND_CODE_STATIC_ABSBANK; uint32_t entryOff = (k == 0) ? s.entryOff : 0; auto seg = emitOneSeg(img, entryOff, s.base, static_cast(s.num), kind, s.name); blob.insert(blob.end(), seg.begin(), seg.end()); totalPayload += img.size(); } std::ofstream f(output, std::ios::binary); if (!f) die("cannot open '" + output + "' for writing"); f.write(reinterpret_cast(blob.data()), blob.size()); std::fprintf(stderr, "OMF: %zu segments, %zu bytes payload -> %s (%zu bytes total)\n", segs.size(), totalPayload, output.c_str(), blob.size()); return 0; } // Legacy single-segment mode (--input/--map/--base). if (input.empty() || mapFile.empty() || !baseSet) usage(argv[0]); auto image = readFile(input); auto syms = readMap(mapFile); auto it = syms.find(entry); if (it == syms.end()) die("entry symbol '" + entry + "' not in map"); uint32_t entryAddr = it->second; if (entryAddr < base || entryAddr >= base + image.size()) die("entry symbol outside linked image"); uint32_t entryOff = entryAddr - base; if (name.empty()) { // Default name: output basename without extension. size_t slash = output.find_last_of('/'); std::string base_n = (slash == std::string::npos) ? output : output.substr(slash + 1); size_t dot = base_n.find_last_of('.'); name = (dot == std::string::npos) ? base_n : base_n.substr(0, dot); } // Pull BSS info from the link816 .map. We embed BSS as zero bytes // in the LCONST data so the OMF Loader allocates and fills the // memory directly (RESSPC zero-fill was unreliable via ExpressLoad // for KIND=CODE segs). link816 page-aligns __bss_start upward // from rodata-end so there's typically a gap; we pad with zeros. uint32_t bssSize = 0, bssStart = 0, bssGap = 0; auto bssSizeIt = syms.find("__bss_size"); auto bssStartIt = syms.find("__bss_start"); if (bssSizeIt != syms.end()) bssSize = bssSizeIt->second; if (bssStartIt != syms.end()) bssStart = bssStartIt->second; if (bssStart >= base) { uint32_t bssOffInImage = bssStart - base; if (bssOffInImage > image.size()) bssGap = bssOffInImage - (uint32_t)image.size(); } auto blob = expressload ? emitOmfExpressLoad(image, entryOff, name, stackSize, bssSize, bssGap) : emitOMF(image, entryOff, name, stackSize, bssSize, bssGap); std::ofstream f(output, std::ios::binary); if (!f) die("cannot open '" + output + "' for writing"); f.write(reinterpret_cast(blob.data()), blob.size()); // Segment count: 1 user CODE seg; +1 for ExpressLoad wrapper; +1 // when --stack-size adds a ~Direct DP/Stack seg. int segCount = 1; if (expressload) segCount++; if (stackSize != 0) segCount++; std::fprintf(stderr, "OMF: %d segment%s%s, %zu bytes payload, entry='%s' at +0x%x -> %s " "(%zu bytes total)\n", segCount, segCount == 1 ? "" : "s", expressload ? " (ExpressLoad)" : "", image.size(), entry.c_str(), entryOff, output.c_str(), blob.size()); return 0; }