Files
ww/selfhost/cmd/w6l/obj.ww
Hojun-Cho c64c478e3f w6l/obj: write diagnostics with str .len (fix truncated-newline off-by-one)
The 5 literal os.write diagnostics in obj.ww ("cannot read object",
"missing .text", "missing .symtab", and two "duplicate symbol") passed
hand-counted byte lengths that were each short by one, dropping the
trailing '\n' so every diagnostic printed without its newline. Replace
each magic length with the string's own .len via the local-binding
idiom (let m: str = "..."; os.write(2, m.ptr, m.len: u64);) — the
established wcc/err.ww + w6c/w6l/main.ww pattern — which fixes the
off-by-one and closes the hand-count class by construction. Uses
str-variable .len (correct on both stages), not "literal".len (cstage
miscompile, #14), so this is byte-identical cs==ww.

Also fold two trivially-safe nested-if collapses in the same file:
the archive-member skip guard (three sequential `if (first != ...)`
with no else → one &&-chain) and the text/data exclusivity guard
(`if (intext) { if (indt) ...`→ `if (intext && indt)`).

Verified: w6l_ww on a missing object now writes the full
"w6l: cannot read object\n"; w6c and w6c_ww emit byte-identical asm
for the regenerated main.combined.ww.
2026-06-03 00:01:26 +09:00

619 lines
18 KiB
Plaintext

// selfhost/cmd/w6l/obj.ww — port of cmd/w6l/obj.c.
//
// Loads relocatable ELF64 .o files emitted by w6a, appends .text to
// the combined image, and pulls in symbols + relocations with
// offsets adjusted to the combined section.
//
// Also handles SysV `ar` archives (libwwrt.a). The two-pass loader
// indexes members on the first pass and iteratively pulls members
// that define currently-undefined symbols on subsequent passes.
package w6l;
import os;
import rt;
import strings;
import sym;
def ET_REL: i32 = 1;
def EM_X86_64: i32 = 62;
def SHT_PROGBITS: i32 = 1;
def SHT_SYMTAB: i32 = 2;
def SHT_STRTAB: i32 = 3;
def SHT_RELA: i32 = 4;
// ---- little-endian byte readers ----------------------------------------
// w6a/w6l use straight LE on amd64. Reading via byte offsets keeps us off
// the cgen's u16 field-load story for now (MOVZBQ exists; MOVZWQ doesn't).
fn rdu16(p: *u8, off: u64) u16 = {
let b0: u16 = p[off]: u16;
let b1: u16 = p[off + 1u64]: u16;
return b0 | (b1 << 8u16);
};
fn rdu32(p: *u8, off: u64) u32 = {
let b0: u32 = p[off]: u32;
let b1: u32 = p[off + 1u64]: u32;
let b2: u32 = p[off + 2u64]: u32;
let b3: u32 = p[off + 3u64]: u32;
return b0 | (b1 << 8u32) | (b2 << 16u32) | (b3 << 24u32);
};
fn rdu64(p: *u8, off: u64) u64 = {
let lo: u64 = rdu32(p, off): u64;
let hi: u64 = rdu32(p, off + 4u64): u64;
return lo | (hi << 32u64);
};
// ---- ELF64 section header offsets (40 bytes total) --------------------
def SHDR_SIZE: u64 = 64u64; // sizeof(Shdr) per ELF64 spec
def SHDR_NAME: u64 = 0u64;
def SHDR_TYPE: u64 = 4u64;
def SHDR_OFFSET: u64 = 24u64;
def SHDR_SIZE_F: u64 = 32u64;
def SHDR_LINK: u64 = 40u64;
// ELF64 ehdr field offsets
def EHDR_SIZE: u64 = 64u64;
def EHDR_TYPE: u64 = 16u64;
def EHDR_MACHINE: u64 = 18u64;
def EHDR_SHOFF: u64 = 40u64;
def EHDR_SHENTSIZE: u64 = 58u64;
def EHDR_SHNUM: u64 = 60u64;
def EHDR_SHSTRNDX: u64 = 62u64;
// ELF64 sym entry: 24 bytes
def SYM_SIZE: u64 = 24u64;
def SYM_NAME: u64 = 0u64;
def SYM_INFO: u64 = 4u64;
def SYM_SHNDX: u64 = 6u64;
def SYM_VALUE: u64 = 8u64;
// ELF64 RELA entry: 24 bytes
def RELA_SIZE: u64 = 24u64;
def RELA_OFFSET: u64 = 0u64;
def RELA_INFO: u64 = 8u64;
def RELA_ADDEND: u64 = 16u64;
// ---- file slurp --------------------------------------------------------
fn slurp(path: *u8) (*u8, u64) = {
let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32);
if (fd < 0) { return nil, 0u64; };
let szr: (i64 | os.oserror) = os.filesize(fd);
let n: i64 = 0i64;
match (szr) {
case let v: i64 => n = v;
case let e: os.oserror => { os.close(fd); return nil, 0u64; };
};
let buf: []u8 = alloc([], n: u64)!;
buf.len = n: i32;
let rr: (i64 | os.oserror) = os.readall(fd, buf.ptr, n: u64);
os.close(fd);
let got: i64 = 0i64;
match (rr) {
case let v: i64 => got = v;
case let e: os.oserror => return nil, 0u64;
};
if (got != n) { return nil, 0u64; };
return buf.ptr, n: u64;
};
// ---- text buffer growth ------------------------------------------------
fn emittext(l: *lnk, src: *u8, n: u64) void = {
if (l.textlen + n > l.textcap) {
let nc: u64 = l.textcap;
if (nc == 0u64) { nc = 4096u64; };
for (nc < l.textlen + n) { nc = nc * 2u64; };
// Grow by mmap'ing a fresh region and copying. The old buffer
// is leaked into the page allocator; for a linker run this is
// trivial waste.
let nb: []u8 = alloc([], nc)!;
let i: u64 = 0u64;
for (i < l.textlen) {
nb[i] = l.text[i];
i += 1u64;
};
l.text = nb.ptr;
l.textcap = nc;
};
let i: u64 = 0u64;
for (i < n) {
l.text[l.textlen + i] = src[i];
i += 1u64;
};
l.textlen += n;
};
fn emitdata(l: *lnk, src: *u8, n: u64) void = {
if (l.datalen + n > l.datacap) {
let nc: u64 = l.datacap;
if (nc == 0u64) { nc = 256u64; };
for (nc < l.datalen + n) { nc = nc * 2u64; };
let nb: []u8 = alloc([], nc)!;
let i: u64 = 0u64;
for (i < l.datalen) {
nb[i] = l.data[i];
i += 1u64;
};
l.data = nb.ptr;
l.datacap = nc;
};
let i: u64 = 0u64;
for (i < n) {
l.data[l.datalen + i] = src[i];
i += 1u64;
};
l.datalen += n;
};
// ---- C-string helpers --------------------------------------------------
fn cstrlen(p: *u8) u64 = {
let n: u64 = 0u64;
for (p[n] != 0u8) { n += 1u64; };
return n;
};
// pathstr — view a NUL-terminated *u8 as a str. Bridges argv-style
// callers to lib/os entrypoints (str post-task-#23). Shared with
// main.ww and dyn.ww via the w6l bundle.
fn pathstr(p: *u8) str = {
let r: str;
r.ptr = p;
r.len = cstrlen(p): i32;
return r;
};
fn cstreq(p: *u8, lit: str) bool = {
let n: u64 = lit.len: u64;
let i: u64 = 0u64;
for (i < n) {
let li: i32 = i: i32;
if (p[i] != lit[li]) { return false; };
i += 1u64;
};
if (p[i] != 0u8) { return false; };
return true;
};
// Build a ww str from a NUL-terminated *u8 (for passing to intern).
fn cstrtostr(p: *u8) str = {
let n: u64 = cstrlen(p);
let view: str;
view.ptr = p;
view.len = n: i32;
return strings.dup(view);
};
// ---- archive (SysV ar) types and helpers -------------------------------
//
// Each archive member starts with a 60-byte ar_hdr. The fields we care
// about are the first byte (member type) and the size at offset 48 (a
// 10-byte, space-padded decimal). Member bodies are 2-byte aligned.
type defent = struct {
name: str,
dnext: *defent,
};
type armember = struct {
data: *u8, // owned heap copy of the member's ELF bytes
size: u64,
defs: *defent, // linked list of defined globals
loaded: i32,
mnext: *armember,
};
fn isarchive(p: *u8, len: u64) bool = {
if (len < 8u64) { return false; };
if (p[0u64] != '!' || p[1u64] != '<' || p[2u64] != 'a' || p[3u64] != 'r' ||
p[4u64] != 'c' || p[5u64] != 'h' || p[6u64] != '>' ||
p[7u64] != '\n') {
return false;
};
return true;
};
// arfield — parse a space-padded decimal integer of width n.
fn arfield(p: *u8, n: u64) u64 = {
let v: u64 = 0u64;
let i: u64 = 0u64;
for (i < n) {
let c: u8 = p[i];
if (c < 48u8) { return v; }; // space, NUL, etc.
if (c > 57u8) { return v; };
v = v * 10u64 + ((c - 48u8): u64);
i += 1u64;
};
return v;
};
// elfglobals — return a linked list of names of globally-defined
// (STB_GLOBAL) symbols whose section is `.text`. Names are owned
// heap copies, so the source ELF buffer can be freed afterward.
fn elfglobals(buf: *u8, len: u64) *defent = {
if (len < EHDR_SIZE) { return nil; };
if (buf[0u64] != 127u8) { return nil; };
if (buf[1u64] != 'E') { return nil; };
if (buf[2u64] != 'L') { return nil; };
if (buf[3u64] != 'F') { return nil; };
let shoff: u64 = rdu64(buf, EHDR_SHOFF);
let shnum: u32 = rdu16(buf, EHDR_SHNUM): u32;
let shstrndx: u32 = rdu16(buf, EHDR_SHSTRNDX): u32;
let shstrshoff: u64 = rdu64(buf, shoff + (shstrndx: u64) * SHDR_SIZE + SHDR_OFFSET);
let shstr: *u8 = buf + shstrshoff;
let idxtext: i32 = -1;
let idxdata: i32 = -1;
let idxsymtab: i32 = -1;
let i: u32 = 0u32;
for (i < shnum) {
let secoff: u64 = shoff + (i: u64) * SHDR_SIZE;
let shtype: u32 = rdu32(buf, secoff + SHDR_TYPE);
let shname: u32 = rdu32(buf, secoff + SHDR_NAME);
let nm: *u8 = shstr + (shname: u64);
if (shtype == SHT_PROGBITS: u32) {
if (cstreq(nm, ".text")) { idxtext = i: i32; };
if (cstreq(nm, ".data")) { idxdata = i: i32; };
};
if (shtype == SHT_SYMTAB: u32) { idxsymtab = i: i32; };
i += 1u32;
};
if (idxtext < 0) { return nil; };
if (idxsymtab < 0) { return nil; };
let symsh: u64 = shoff + (idxsymtab: u64) * SHDR_SIZE;
let symoff: u64 = rdu64(buf, symsh + SHDR_OFFSET);
let symsize: u64 = rdu64(buf, symsh + SHDR_SIZE_F);
let symlink: u32 = rdu32(buf, symsh + SHDR_LINK);
let nsyms: u64 = symsize / SYM_SIZE;
let strsh: u64 = shoff + (symlink: u64) * SHDR_SIZE;
let stroff: u64 = rdu64(buf, strsh + SHDR_OFFSET);
let strtab: *u8 = buf + stroff;
let head: *defent = nil;
let si: u64 = 1u64;
for (si < nsyms) {
let symp: u64 = symoff + si * SYM_SIZE;
let stname: u32 = rdu32(buf, symp + SYM_NAME);
let stinfo: u8 = buf[symp + SYM_INFO];
let stshndx: u16 = rdu16(buf, symp + SYM_SHNDX);
let bind: u32 = (stinfo: u32) >> 4u32;
// STB_GLOBAL = 1; defined in .text or .data. Both are
// included so an archive member that owns a data global
// gets pulled in when something references it.
if (bind == 1u32) {
if (stshndx != 0u16) {
let intext: bool = (stshndx: i32) == idxtext;
let indt: bool = false;
if (idxdata >= 0) {
indt = (stshndx: i32) == idxdata;
};
if (intext || indt) {
let nmp: *u8 = strtab + (stname: u64);
if (nmp[0u64] != 0u8) {
let nm: str = cstrtostr(nmp);
let de: *defent = alloc(defent { name = nm, dnext = head })!;
head = de;
};
};
};
};
si += 1u64;
};
return head;
};
// memberdefinesundef — true if any of m's defined globals matches a
// currently-undefined symbol in the linker's symbol table. Names not
// already interned are uninteresting (the link doesn't need them yet).
fn memberdefinesundef(l: *lnk, m: *armember) bool = {
let de: *defent = m.defs;
for (de != nil) {
let s: *lsym = lookup(l, de.name);
if (s != nil) {
if (s.defined == 0) { return true; };
};
de = de.dnext;
};
return false;
};
// loadarchive — port of cmd/w6l/obj.c:load_archive.
//
// Pass 1 indexes every regular member. Pass 2 iteratively pulls in any
// member that supplies a currently-undefined symbol; each pull may
// introduce fresh undefs, so we loop until quiescent.
fn loadarchive(l: *lnk, path: *u8, buf: *u8, len: u64) i32 = {
let head: *armember = nil;
let tail: *armember = nil;
let pos: u64 = 8u64; // past "!<arch>\n"
for (pos + 60u64 <= len) {
let hdrsize: u64 = arfield(buf + pos + 48u64, 10u64);
let hdrend: u64 = pos + 60u64;
if (hdrend + hdrsize > len) { break; };
let first: u8 = buf[pos];
// Skip the symbol table ('/'), long-name table ('//'), and
// any padding entries (NUL or space leading byte).
if (first != '/' && first != 0u8 && first != ' ') {
let m: *armember = alloc(armember { size = hdrsize })!;
let mbs: []u8 = alloc([], hdrsize)!;
let mb: *u8 = mbs.ptr;
let i: u64 = 0u64;
for (i < hdrsize) {
mb[i] = buf[hdrend + i];
i += 1u64;
};
m.data = mb;
m.defs = elfglobals(mb, hdrsize);
if (head == nil) { head = m; }
else { tail.mnext = m; };
tail = m;
};
pos = hdrend + hdrsize;
if ((hdrsize & 1u64) != 0u64) { pos = pos + 1u64; };
};
let changed: i32 = 1;
for (changed != 0) {
changed = 0;
let m: *armember = head;
for (m != nil) {
if (m.loaded == 0) {
if (memberdefinesundef(l, m)) {
if (loadimage(l, path, m.data, m.size) == 0) {
m.loaded = 1;
changed = 1;
};
};
};
m = m.mnext;
};
};
return 0;
};
// ---- main loader -------------------------------------------------------
export fn load(l: *lnk, path: *u8) i32 = {
let bufp: *u8;
let buflen: u64;
bufp, buflen = slurp(path);
if (bufp == nil) {
let m: str = "w6l: cannot read object\n";
os.write(2, m.ptr, m.len: u64);
return -1;
};
if (isarchive(bufp, buflen)) {
return loadarchive(l, path, bufp, buflen);
};
return loadimage(l, path, bufp, buflen);
};
fn loadimage(l: *lnk, path: *u8, buf: *u8, len: u64) i32 = {
if (len < EHDR_SIZE) { return -1; };
// magic: 0x7f, 'E', 'L', 'F'
if (buf[0u64] != 127u8) { return -1; };
if (buf[1u64] != 'E') { return -1; };
if (buf[2u64] != 'L') { return -1; };
if (buf[3u64] != 'F') { return -1; };
if (buf[4u64] != 2u8) { return -1; }; // ELFCLASS64
if (rdu16(buf, EHDR_TYPE) != ET_REL: u16) { return -1; };
if (rdu16(buf, EHDR_MACHINE) != EM_X86_64: u16) { return -1; };
let shoff: u64 = rdu64(buf, EHDR_SHOFF);
let shnum: u32 = rdu16(buf, EHDR_SHNUM): u32;
let shstrndx: u32 = rdu16(buf, EHDR_SHSTRNDX): u32;
let shstrshoff: u64 = rdu64(buf, shoff + (shstrndx: u64) * SHDR_SIZE + SHDR_OFFSET);
let shstr: *u8 = buf + shstrshoff;
// find .text, .data, .symtab, .rela.text, .rela.data
let idxtext: i32 = -1;
let idxdata: i32 = -1;
let idxsymtab: i32 = -1;
let idxrela: i32 = -1;
let idxrelad: i32 = -1;
let i: u32 = 0u32;
for (i < shnum) {
let secoff: u64 = shoff + (i: u64) * SHDR_SIZE;
let shtype: u32 = rdu32(buf, secoff + SHDR_TYPE);
let shname: u32 = rdu32(buf, secoff + SHDR_NAME);
let nm: *u8 = shstr + (shname: u64);
if (shtype == SHT_PROGBITS: u32) {
if (cstreq(nm, ".text")) { idxtext = i: i32; };
if (cstreq(nm, ".data")) { idxdata = i: i32; };
};
if (shtype == SHT_SYMTAB: u32) { idxsymtab = i: i32; };
if (shtype == SHT_RELA: u32) {
if (cstreq(nm, ".rela.text")) { idxrela = i: i32; };
if (cstreq(nm, ".rela.data")) { idxrelad = i: i32; };
};
i += 1u32;
};
if (idxtext < 0) {
let m: str = "w6l: missing .text\n";
os.write(2, m.ptr, m.len: u64);
return -1;
};
if (idxsymtab < 0) {
let m: str = "w6l: missing .symtab\n";
os.write(2, m.ptr, m.len: u64);
return -1;
};
let textsh: u64 = shoff + (idxtext: u64) * SHDR_SIZE;
let textoff: u64 = rdu64(buf, textsh + SHDR_OFFSET);
let textsize: u64 = rdu64(buf, textsh + SHDR_SIZE_F);
let symsh: u64 = shoff + (idxsymtab: u64) * SHDR_SIZE;
let symoff: u64 = rdu64(buf, symsh + SHDR_OFFSET);
let symsize: u64 = rdu64(buf, symsh + SHDR_SIZE_F);
let symlink: u32 = rdu32(buf, symsh + SHDR_LINK);
let nsyms: u64 = symsize / SYM_SIZE;
let strsh: u64 = shoff + (symlink: u64) * SHDR_SIZE;
let stroff: u64 = rdu64(buf, strsh + SHDR_OFFSET);
let strtab: *u8 = buf + stroff;
let datasize: u64 = 0u64;
let dataoff: u64 = 0u64;
if (idxdata >= 0) {
let datash: u64 = shoff + (idxdata: u64) * SHDR_SIZE;
dataoff = rdu64(buf, datash + SHDR_OFFSET);
datasize = rdu64(buf, datash + SHDR_SIZE_F);
};
// Track this object.
let ob: *lobj = alloc(lobj {
path = cstrtostr(path),
buf = buf,
len = len,
textoff = l.textlen,
textsize = textsize,
dataoff = l.datalen,
datasize = datasize,
onext = l.objs,
})!;
l.objs = ob;
// Append .text bytes to the combined image.
emittext(l, buf + textoff, textsize);
// Append .data bytes (if present) to the combined .data buffer.
if (idxdata >= 0) {
if (datasize > 0u64) {
emitdata(l, buf + dataoff, datasize);
};
};
// Walk symbols. We don't keep a per-object map[] of *lsym. Instead
// the reloc loop re-walks symtab and re-interns by name. Simpler
// than dancing around the cgen's u64-shift gaps.
let si: u64 = 1u64; // skip index 0 (always undef sentinel)
for (si < nsyms) {
let symp: u64 = symoff + si * SYM_SIZE;
let stname: u32 = rdu32(buf, symp + SYM_NAME);
let stshndx: u16 = rdu16(buf, symp + SYM_SHNDX);
let stvalue: u64 = rdu64(buf, symp + SYM_VALUE);
let nmp: *u8 = strtab + (stname: u64);
if (nmp[0u64] != 0u8) {
let nm: str = cstrtostr(nmp);
let gs: *lsym = intern(l, nm);
if (stshndx != 0u16) {
let intext: bool = (stshndx: i32) == idxtext;
let indt: bool = false;
if (idxdata >= 0) {
indt = (stshndx: i32) == idxdata;
};
if (intext && indt) { indt = false; };
if (intext) {
if (gs.defined != 0) {
let m: str = "w6l: duplicate symbol\n";
os.write(2, m.ptr, m.len: u64);
l.errs += 1;
} else {
gs.defined = 1;
gs.owner = ob;
gs.idxinowner = si: i32;
gs.val = ob.textoff + stvalue;
};
};
if (indt) {
if (gs.defined != 0) {
let m: str = "w6l: duplicate symbol\n";
os.write(2, m.ptr, m.len: u64);
l.errs += 1;
} else {
gs.defined = 1;
gs.indata = 1;
gs.owner = ob;
gs.idxinowner = si: i32;
gs.val = ob.dataoff + stvalue;
};
};
};
};
si += 1u64;
};
// Per-object relocation collection.
if (idxrela >= 0) {
let relash: u64 = shoff + (idxrela: u64) * SHDR_SIZE;
let relaoff: u64 = rdu64(buf, relash + SHDR_OFFSET);
let relasize: u64 = rdu64(buf, relash + SHDR_SIZE_F);
let nrel: u64 = relasize / RELA_SIZE;
let ri: u64 = 0u64;
for (ri < nrel) {
let rp: u64 = relaoff + ri * RELA_SIZE;
let roff: u64 = rdu64(buf, rp + RELA_OFFSET);
let rinfo: u64 = rdu64(buf, rp + RELA_INFO);
let raddend: u64 = rdu64(buf, rp + RELA_ADDEND);
let rsymidx: u32 = (rinfo >> 32u64): u32;
let rkind: i32 = ((rinfo & 4294967295u64): u32): i32;
let nr: *lrel = alloc(lrel {
off = ob.textoff + roff,
section = 0,
kind = rkind,
addend = raddend: i64,
rnext = l.rels,
})!;
// Look up the referenced sym by name (re-walk symtab).
if ((rsymidx: u64) < nsyms) {
let sp: u64 = symoff + (rsymidx: u64) * SYM_SIZE;
let sname: u32 = rdu32(buf, sp + SYM_NAME);
let snm: *u8 = strtab + (sname: u64);
if (snm[0u64] != 0u8) {
let nm: str = cstrtostr(snm);
nr.sym = intern(l, nm);
};
};
l.rels = nr;
ri += 1u64;
};
};
// Data-reloc collection. Offsets land in .data, shifted by
// this object's data_off so they index the combined buffer.
if (idxrelad >= 0) {
let relash: u64 = shoff + (idxrelad: u64) * SHDR_SIZE;
let relaoff: u64 = rdu64(buf, relash + SHDR_OFFSET);
let relasize: u64 = rdu64(buf, relash + SHDR_SIZE_F);
let nrel: u64 = relasize / RELA_SIZE;
let ri: u64 = 0u64;
for (ri < nrel) {
let rp: u64 = relaoff + ri * RELA_SIZE;
let roff: u64 = rdu64(buf, rp + RELA_OFFSET);
let rinfo: u64 = rdu64(buf, rp + RELA_INFO);
let raddend: u64 = rdu64(buf, rp + RELA_ADDEND);
let rsymidx: u32 = (rinfo >> 32u64): u32;
let rkind: i32 = ((rinfo & 4294967295u64): u32): i32;
let nr: *lrel = alloc(lrel {
off = ob.dataoff + roff,
section = 1,
kind = rkind,
addend = raddend: i64,
rnext = l.rels,
})!;
if ((rsymidx: u64) < nsyms) {
let sp: u64 = symoff + (rsymidx: u64) * SYM_SIZE;
let sname: u32 = rdu32(buf, sp + SYM_NAME);
let snm: *u8 = strtab + (sname: u64);
if (snm[0u64] != 0u8) {
let nm: str = cstrtostr(snm);
nr.sym = intern(l, nm);
};
};
l.rels = nr;
ri += 1u64;
};
};
return 0;
};