1218 lines
60 KiB
C++
1218 lines
60 KiB
C++
// 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 <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <map>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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<RelocSite> gReloc24Sites;
|
|
|
|
// Phase C.2 — inter-segment IMM24 site for cINTERSEG (0xF6) emission.
|
|
// Populated from link816's per-seg sidecar (`<reloc-out>.seg<N>.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<uint8_t> readFile(const std::string &path) {
|
|
std::ifstream f(path, std::ios::binary);
|
|
if (!f) die("cannot open '" + path + "' for reading");
|
|
return std::vector<uint8_t>((std::istreambuf_iterator<char>(f)),
|
|
std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
static std::map<std::string, uint32_t> readMap(const std::string &path) {
|
|
std::map<std::string, uint32_t> 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<uint8_t> &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<uint8_t> &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<uint8_t> emitOneSeg(const std::vector<uint8_t> &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<RelocSite> &relocSites,
|
|
const std::vector<InterRelocSite> &interSites);
|
|
|
|
// Adapter: legacy single-seg call sites that read from `gReloc24Sites`.
|
|
static std::vector<uint8_t> emitOneSeg(const std::vector<uint8_t> &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<InterRelocSite> noInter;
|
|
return emitOneSeg(image, entryOff, org, segNum, kind, name,
|
|
bssSize, bssGap, gReloc24Sites, noInter);
|
|
}
|
|
|
|
static std::vector<uint8_t> emitOneSeg(const std::vector<uint8_t> &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<RelocSite> &relocSites,
|
|
const std::vector<InterRelocSite> &interSites) {
|
|
std::vector<uint8_t> 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<uint8_t> 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<uint32_t>(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<uint8_t> loadName(10, 0x20); // 10 spaces
|
|
std::string segNameTxt = name.substr(0, 10); // truncate to LABLEN
|
|
std::vector<uint8_t> 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<uint16_t>(
|
|
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<uint32_t>(combined.size());
|
|
const uint32_t BYTECNT = DISPDATA + static_cast<uint32_t>(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<uint8_t> 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<uint8_t> 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<uint8_t> emitDpStackSeg(uint32_t length, uint16_t segNum) {
|
|
std::vector<uint8_t> 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<uint8_t> loadName(LABLEN_VAL, 0x20);
|
|
std::vector<uint8_t> 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<uint16_t>(
|
|
DISPNAME + loadName.size() + segName.size());
|
|
const uint32_t LENGTH = length; // memory size requested
|
|
const uint32_t BYTECNT = DISPDATA + static_cast<uint32_t>(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<uint8_t> 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<uint8_t> 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<uint8_t> emitOMF(const std::vector<uint8_t> &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<uint8_t> 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<uint8_t> image;
|
|
uint32_t entryOffset = 0;
|
|
std::string name;
|
|
uint32_t bssSize = 0;
|
|
uint32_t bssGap = 0;
|
|
std::vector<RelocSite> relocSites;
|
|
std::vector<InterRelocSite> interSites; // Phase C.2 cINTERSEG sites
|
|
};
|
|
|
|
static std::vector<uint8_t> emitOmfExpressLoad(
|
|
const std::vector<UserSeg> &users,
|
|
uint32_t stackSize = 0);
|
|
|
|
// Legacy single-user-seg adapter. Reads cRELOC sites from the global
|
|
// `gReloc24Sites` (filled by --relocs).
|
|
static std::vector<uint8_t> emitOmfExpressLoad(
|
|
const std::vector<uint8_t> &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<UserSeg>{u}, stackSize);
|
|
}
|
|
|
|
static std::vector<uint8_t> emitOmfExpressLoad(
|
|
const std::vector<UserSeg> &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<std::vector<uint8_t>> 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<uint8_t> 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<uint32_t> userDataOffs;
|
|
std::vector<uint32_t> userCRelocOffs;
|
|
std::vector<uint32_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint32_t>(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<uint32_t>(std::stoul(block.substr(p, e - p),
|
|
nullptr, 0));
|
|
}
|
|
|
|
static std::vector<ManifestSeg> parseManifest(const std::string &path) {
|
|
std::ifstream f(path);
|
|
if (!f) die("cannot open '" + path + "' for reading");
|
|
std::string text((std::istreambuf_iterator<char>(f)),
|
|
std::istreambuf_iterator<char>());
|
|
std::vector<ManifestSeg> 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<uint32_t>(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
|
|
// (<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);
|
|
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<UserSeg> 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<N>.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<uint8_t> raw(
|
|
(std::istreambuf_iterator<char>(pf)),
|
|
std::istreambuf_iterator<char>());
|
|
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<const char *>(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<uint8_t> 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<uint16_t>(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<const char *>(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<const char *>(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;
|
|
}
|