Files
ww/selfhost/cmd/6l/main.combined.ww
Hojun-Cho 1657bdeda3 ww: import toolchain — C bootstrap + ww-side self-host (phases 0-10)
C bootstrap (phases 0-9):
  cmd/wwc, cmd/6c, cmd/6a, cmd/6l, cmd/ww, rt, lib/*.

ww-side self-host (phase 10):
  selfhost/cmd/wwc — ww-cgen frontend; bootstrap fixed point.
  selfhost/cmd/6a  — assembler; byte-identical to C 6a (test 991).
  selfhost/cmd/6l  — linker w/ archive (.a) support; byte-identical
                     to C 6l (test 992).
  selfhost/cmd/ww  — driver (build/run/version); byte-identical to
                     C ww (test 993).

make test: 15/15. make bootstrap: ww2.s == ww3.s, ww2.o == ww3.o,
ww2 == ww3 byte-identical, with the full ww-tooled chain.
2026-05-11 02:17:47 +09:00

1078 lines
30 KiB
Plaintext

// os — process and filesystem facade. The body of each call lands
// either in libwwrt.a (rt_syscall trampoline) or libc bindings,
// depending on how the program was linked.
@symbol("rt_syscall") fn syscall0(num: i64) i64;
@symbol("rt_syscall") fn syscall1(num: i64, a: i64) i64;
@symbol("rt_syscall") fn syscall2(num: i64, a: i64, b: i64) i64;
@symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64;
@symbol("rt_alloc") fn alloc(n: u64) *void;
@symbol("rt_free") fn free(p: *void, n: u64) void;
@symbol("rt_abort") fn abort(msg: str) void;
// Hare-style runtime check. Caller passes a message that's printed
// to stderr before exit(1).
export fn assert(cond: bool, msg: str) void = {
if (!cond) { abort(msg); };
};
def SYS_READ: i64 = 0;
def SYS_WRITE: i64 = 1;
def SYS_OPEN: i64 = 2;
def SYS_CLOSE: i64 = 3;
def SYS_LSEEK: i64 = 8;
def SYS_EXIT: i64 = 60;
// open(2) flags. Linux values, matching <fcntl.h>.
def O_RDONLY: i32 = 0;
def O_WRONLY: i32 = 1;
def O_RDWR: i32 = 2;
def O_CREAT: i32 = 64; // 0x40
def O_TRUNC: i32 = 512; // 0x200
// lseek(2) whence.
def SEEK_SET: i32 = 0;
def SEEK_CUR: i32 = 1;
def SEEK_END: i32 = 2;
export fn exit(code: i32) void = {
syscall1(SYS_EXIT, code: i64);
};
// Raw, non-fallible primitives. These return Linux's int conventions
// (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a
// Hare-style fallible API use the wrappers below.
export fn write(fd: i32, buf: *u8, n: u64) i64 = {
return syscall3(SYS_WRITE, fd: i64, buf: i64, n: i64);
};
export fn read(fd: i32, buf: *u8, n: u64) i64 = {
return syscall3(SYS_READ, fd: i64, buf: i64, n: i64);
};
export fn close(fd: i32) i32 = {
return syscall1(SYS_CLOSE, fd: i64): i32;
};
// Fallible wrappers. The error variant is a plain str (Plan 9 errstr
// model, see lib/errors); the sum type makes success/failure explicit
// without overloading length-zero.
export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | str) = {
let r: i64 = read(fd, buf, n);
if (r < 0) { return "read failed"; };
return r;
};
export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | str) = {
let r: i64 = write(fd, buf, n);
if (r < 0) { return "write failed"; };
return r;
};
// open — Linux open(2). Path must be NUL-terminated; callers using ww
// `str` must ensure the bytes are followed by a 0 byte (literals are,
// arena-copied paths usually are by construction). Returns -errno on
// failure, fd otherwise. Higher-level callers prefer `tryopen`.
export fn open(path: *u8, flags: i32, mode: i32) i32 = {
return syscall3(SYS_OPEN, path: i64, flags: i64, mode: i64): i32;
};
export fn tryopen(path: *u8, flags: i32, mode: i32) (i32 | str) = {
let fd: i32 = open(path, flags, mode);
if (fd < 0) { return "open failed"; };
return fd;
};
// lseek — set/inspect the fd's position. Returns the new offset or
// a negative errno. We use this for fstat-free file-size discovery
// (open ⇒ lseek to end ⇒ lseek back).
export fn lseek(fd: i32, off: i64, whence: i32) i64 = {
return syscall3(SYS_LSEEK, fd: i64, off, whence: i64);
};
// filesize — convenience: returns the byte length of an open fd by
// seeking to the end and back. -1 on error.
export fn filesize(fd: i32) i64 = {
let end: i64 = lseek(fd, 0i64, SEEK_END);
if (end < 0) { return -1i64; };
let r: i64 = lseek(fd, 0i64, SEEK_SET);
if (r < 0) { return -1i64; };
return end;
};
// readfull — keep reading until `n` bytes have arrived or the fd
// closes early. Returns bytes read (0..=n) or -1 on read error.
export fn readfull(fd: i32, buf: *u8, n: u64) i64 = {
let got: u64 = 0u64;
for (got < n) {
let r: i64 = read(fd, buf + got, n - got);
if (r < 0) { return -1i64; };
if (r == 0) { return got: i64; }; // short read: caller decides
got += r: u64;
};
return got: i64;
};
// writefull — keep writing until `n` bytes have been accepted or the
// fd refuses progress. Returns bytes written or -1.
export fn writefull(fd: i32, buf: *u8, n: u64) i64 = {
let sent: u64 = 0u64;
for (sent < n) {
let r: i64 = write(fd, buf + sent, n - sent);
if (r < 0) { return -1i64; };
if (r == 0) { return sent: i64; };
sent += r: u64;
};
return sent: i64;
};
// selfhost/cmd/wwc/mem.ww — port of cmd/wwc/mem.c.
//
// Bump arena allocator. Backed by the runtime page allocator
// (rt_alloc / rt_free), no libc. Each chunk is mmap'd; when the
// current chunk runs out we link a fresh one. Freeing the arena
// unmaps the chain.
//
// Memory handed out is 16-byte aligned. The C version under
// cmd/wwc/ is retained until the three-stage bootstrap diffs clean.
use os;
def ALIGN: u64 = 16u64;
def INIT_CHUNK: u64 = 65536u64;
def MAX_CHUNK: u64 = 4194304u64;
def ARENA_SZ: u64 = 48u64; // sizeof(arena), kept in sync below
type arena = struct {
buf: *u8,
off: u64,
cap: u64,
next: *arena,
total: u64,
};
fn roundup(n: u64, a: u64) u64 = {
return (n + a - 1u64) & ~(a - 1u64);
};
export fn newarena() *arena = {
let a: *arena = os.alloc(ARENA_SZ): *arena;
a.buf = os.alloc(INIT_CHUNK): *u8;
a.off = 0u64;
a.cap = INIT_CHUNK;
a.next = nil;
a.total = 0u64;
return a;
};
// Grow: link a fresh chunk in front of the head. We push the old
// chunk into `next` so the head always describes the current bump
// region. Chunk size doubles up to MAX_CHUNK.
fn grow(a: *arena, need: u64) bool = {
let want: u64 = a.cap * 2u64;
if (want < need) { want = need; };
if (want > MAX_CHUNK) { want = MAX_CHUNK; };
if (want < need) { return false; }; // single allocation too big
let old: *arena = os.alloc(ARENA_SZ): *arena;
old.buf = a.buf;
old.off = a.off;
old.cap = a.cap;
old.next = a.next;
old.total = 0u64;
a.buf = os.alloc(want): *u8;
a.off = 0u64;
a.cap = want;
a.next = old;
return true;
};
export fn amalloc(a: *arena, n: u64) *void = {
let need: u64 = roundup(n, ALIGN);
if (need > a.cap - a.off) {
if (!grow(a, need)) { return nil; };
};
let p: *u8 = a.buf + a.off;
a.off += need;
a.total += need;
// Zero the region. Plan 9 amalloc zeroes; we mirror that here so
// the checker can assume freshly allocated nodes start at 0.
let i: u64 = 0u64;
for (i < need) {
p[i] = 0u8;
i += 1u64;
};
return p: *void;
};
// astrndup — copy `n` bytes into the arena and produce a NUL-terminated
// view. Returns a `str` whose ptr is arena-owned and whose len is `n`
// (the trailing NUL is past `len`, so callers reading exactly n bytes
// see no padding). Used by the lexer to capture token text.
export fn astrndup(a: *arena, src: *u8, n: u64) str = {
let p: *u8 = amalloc(a, n + 1u64): *u8;
let i: u64 = 0u64;
for (i < n) {
p[i] = src[i];
i += 1u64;
};
p[n] = 0u8;
let r: str;
r.ptr = p;
r.len = n: i32;
return r;
};
export fn freearena(a: *arena) void = {
for (a != nil) {
let next: *arena = a.next;
os.free(a.buf: *void, a.cap);
os.free(a: *void, ARENA_SZ);
a = next;
};
};
// selfhost/cmd/6l/sym.ww — port of cmd/6l/sym.c.
//
// Linker symbol table. Singly-linked list, usually a few hundred
// entries; hashing isn't worth it yet.
use mem;
type lsym = struct {
name: str,
val: u64, // offset within combined .text once linked
defined: i32, // 1 if some lobj defines this symbol
owner: *lobj,
idx_in_owner: i32,
snext: *lsym,
};
type lrel = struct {
off: u64, // offset within combined .text
kind: i32, // R_X86_64_*
sym: *lsym,
addend: i64,
rnext: *lrel,
};
type lobj = struct {
path: str,
buf: *u8, // object bytes
len: u64,
text_off: u64, // offset of .text in combined output
text_size: u64,
onext: *lobj,
};
type lnk = struct {
a: *arena,
objs: *lobj,
syms: *lsym,
rels: *lrel,
text: *u8, // combined .text
textcap: u64,
textlen: u64,
errs: i32,
};
fn streq(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
let i: i32 = 0;
for (i < a.len) {
if (a[i] != b[i]) { return false; };
i += 1;
};
return true;
};
export fn l_intern(l: *lnk, name: str) *lsym = {
let s: *lsym = l.syms;
for (s != nil) {
if (streq(s.name, name)) { return s; };
s = s.snext;
};
let n: *lsym = amalloc(l.a, 64u64): *lsym;
n.name = name;
n.snext = l.syms;
l.syms = n;
return n;
};
export fn l_lookup(l: *lnk, name: str) *lsym = {
let s: *lsym = l.syms;
for (s != nil) {
if (streq(s.name, name)) { return s; };
s = s.snext;
};
return nil;
};
// selfhost/cmd/6l/obj.ww — port of cmd/6l/obj.c.
//
// Loads relocatable ELF64 .o files emitted by 6a, 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.
use os;
use mem;
use 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 ----------------------------------------
// 6a/6l 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 rd_u16(p: *u8, off: u64) u16 = {
let b0: u16 = p[off]: u16;
let b1: u16 = p[off + 1u64]: u16;
return b0 | (b1 << 8u16);
};
fn rd_u32(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 rd_u64(p: *u8, off: u64) u64 = {
let lo: u64 = rd_u32(p, off): u64;
let hi: u64 = rd_u32(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 read_all(path_cs: *u8) (*u8, u64) = {
let fd: i32 = os.open(path_cs, os.O_RDONLY, 0i32);
if (fd < 0) { return nil, 0u64; };
let n: i64 = os.filesize(fd);
if (n < 0i64) { os.close(fd); return nil, 0u64; };
let buf: *u8 = os.alloc(n: u64): *u8;
let got: i64 = os.readfull(fd, buf, n: u64);
os.close(fd);
if (got != n) { return nil, 0u64; };
return buf, n: u64;
};
// ---- text buffer growth ------------------------------------------------
fn emit_text(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 = os.alloc(nc): *u8;
let i: u64 = 0u64;
for (i < l.textlen) {
nb[i] = l.text[i];
i += 1u64;
};
l.text = nb;
l.textcap = nc;
};
let i: u64 = 0u64;
for (i < n) {
l.text[l.textlen + i] = src[i];
i += 1u64;
};
l.textlen += n;
};
// ---- C-string helpers --------------------------------------------------
fn cstrlen(p: *u8) u64 = {
let n: u64 = 0u64;
for (p[n] != 0u8) { n += 1u64; };
return n;
};
fn cstr_eq(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 l_intern).
fn cstr_to_str(a: *arena, p: *u8) str = {
let n: u64 = cstrlen(p);
return astrndup(a, p, n);
};
// ---- 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, // arena copy of the member's ELF bytes
size: u64,
defs: *defent, // linked list of defined globals
loaded: i32,
mnext: *armember,
};
fn is_archive(p: *u8, len: u64) bool = {
if (len < 8u64) { return false; };
if (p[0u64] != 33u8) { return false; }; // '!'
if (p[1u64] != 60u8) { return false; }; // '<'
if (p[2u64] != 97u8) { return false; }; // 'a'
if (p[3u64] != 114u8) { return false; }; // 'r'
if (p[4u64] != 99u8) { return false; }; // 'c'
if (p[5u64] != 104u8) { return false; }; // 'h'
if (p[6u64] != 62u8) { return false; }; // '>'
if (p[7u64] != 10u8) { return false; }; // '\n'
return true;
};
// ar_field — parse a space-padded decimal integer of width n.
fn ar_field(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;
};
// elf_globals — return a linked list of names of globally-defined
// (STB_GLOBAL) symbols whose section is `.text`. Names are arena
// copies, so the source ELF buffer can be freed afterward.
fn elf_globals(a: *arena, buf: *u8, len: u64) *defent = {
if (len < EHDR_SIZE) { return nil; };
if (buf[0u64] != 127u8) { return nil; };
if (buf[1u64] != 69u8) { return nil; };
if (buf[2u64] != 76u8) { return nil; };
if (buf[3u64] != 70u8) { return nil; };
let shoff: u64 = rd_u64(buf, EHDR_SHOFF);
let shnum: u32 = rd_u16(buf, EHDR_SHNUM): u32;
let shstrndx: u32 = rd_u16(buf, EHDR_SHSTRNDX): u32;
let shstr_sh_off: u64 = rd_u64(buf, shoff + (shstrndx: u64) * SHDR_SIZE + SHDR_OFFSET);
let shstr: *u8 = buf + shstr_sh_off;
let idx_text: i32 = -1;
let idx_symtab: i32 = -1;
let i: u32 = 0u32;
for (i < shnum) {
let sh_off: u64 = shoff + (i: u64) * SHDR_SIZE;
let sh_type: u32 = rd_u32(buf, sh_off + SHDR_TYPE);
let sh_name: u32 = rd_u32(buf, sh_off + SHDR_NAME);
let nm: *u8 = shstr + (sh_name: u64);
if (sh_type == SHT_PROGBITS: u32) {
if (cstr_eq(nm, ".text")) { idx_text = i: i32; };
};
if (sh_type == SHT_SYMTAB: u32) { idx_symtab = i: i32; };
i += 1u32;
};
if (idx_text < 0) { return nil; };
if (idx_symtab < 0) { return nil; };
let sym_sh: u64 = shoff + (idx_symtab: u64) * SHDR_SIZE;
let sym_off: u64 = rd_u64(buf, sym_sh + SHDR_OFFSET);
let sym_size: u64 = rd_u64(buf, sym_sh + SHDR_SIZE_F);
let sym_link: u32 = rd_u32(buf, sym_sh + SHDR_LINK);
let nsyms: u64 = sym_size / SYM_SIZE;
let str_sh: u64 = shoff + (sym_link: u64) * SHDR_SIZE;
let str_off: u64 = rd_u64(buf, str_sh + SHDR_OFFSET);
let strtab: *u8 = buf + str_off;
let head: *defent = nil;
let si: u64 = 1u64;
for (si < nsyms) {
let sym_p: u64 = sym_off + si * SYM_SIZE;
let st_name: u32 = rd_u32(buf, sym_p + SYM_NAME);
let st_info: u8 = buf[sym_p + SYM_INFO];
let st_shndx: u16 = rd_u16(buf, sym_p + SYM_SHNDX);
let bind: u32 = (st_info: u32) >> 4u32;
// STB_GLOBAL = 1; defined in .text.
if (bind == 1u32) {
if (st_shndx != 0u16) {
if ((st_shndx: i32) == idx_text) {
let nm_p: *u8 = strtab + (st_name: u64);
if (nm_p[0u64] != 0u8) {
let nm: str = cstr_to_str(a, nm_p);
let de: *defent = amalloc(a, 32u64): *defent;
de.name = nm;
de.dnext = head;
head = de;
};
};
};
};
si += 1u64;
};
return head;
};
// member_defines_undef — 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 member_defines_undef(l: *lnk, m: *armember) bool = {
let de: *defent = m.defs;
for (de != nil) {
let s: *lsym = l_lookup(l, de.name);
if (s != nil) {
if (s.defined == 0) { return true; };
};
de = de.dnext;
};
return false;
};
// load_archive — port of cmd/6l/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 load_archive(l: *lnk, path_cs: *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 hdr_size: u64 = ar_field(buf + pos + 48u64, 10u64);
let hdr_end: u64 = pos + 60u64;
if (hdr_end + hdr_size > 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 != 47u8) { if (first != 0u8) { if (first != 32u8) {
let m: *armember = amalloc(l.a, 48u64): *armember;
m.size = hdr_size;
let mb: *u8 = amalloc(l.a, hdr_size): *u8;
let i: u64 = 0u64;
for (i < hdr_size) {
mb[i] = buf[hdr_end + i];
i += 1u64;
};
m.data = mb;
m.defs = elf_globals(l.a, mb, hdr_size);
m.loaded = 0;
m.mnext = nil;
if (head == nil) { head = m; }
else { tail.mnext = m; };
tail = m;
}; }; };
pos = hdr_end + hdr_size;
if ((hdr_size & 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 (member_defines_undef(l, m)) {
if (load_image(l, path_cs, m.data, m.size) == 0) {
m.loaded = 1;
changed = 1;
};
};
};
m = m.mnext;
};
};
return 0;
};
// ---- main loader -------------------------------------------------------
export fn l_load(l: *lnk, path_cs: *u8) i32 = {
let bufp: *u8;
let buflen: u64;
bufp, buflen = read_all(path_cs);
if (bufp == nil) {
os.write(2, "6l: cannot read object\n".ptr, 23u64);
return -1;
};
if (is_archive(bufp, buflen)) {
return load_archive(l, path_cs, bufp, buflen);
};
return load_image(l, path_cs, bufp, buflen);
};
fn load_image(l: *lnk, path_cs: *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] != 69u8) { return -1; };
if (buf[2u64] != 76u8) { return -1; };
if (buf[3u64] != 70u8) { return -1; };
if (buf[4u64] != 2u8) { return -1; }; // ELFCLASS64
if (rd_u16(buf, EHDR_TYPE) != ET_REL: u16) { return -1; };
if (rd_u16(buf, EHDR_MACHINE) != EM_X86_64: u16) { return -1; };
let shoff: u64 = rd_u64(buf, EHDR_SHOFF);
let shnum: u32 = rd_u16(buf, EHDR_SHNUM): u32;
let shstrndx: u32 = rd_u16(buf, EHDR_SHSTRNDX): u32;
let shstr_sh_off: u64 = rd_u64(buf, shoff + (shstrndx: u64) * SHDR_SIZE + SHDR_OFFSET);
let shstr: *u8 = buf + shstr_sh_off;
// find .text, .symtab, .rela.text
let idx_text: i32 = -1;
let idx_symtab: i32 = -1;
let idx_rela: i32 = -1;
let i: u32 = 0u32;
for (i < shnum) {
let sh_off: u64 = shoff + (i: u64) * SHDR_SIZE;
let sh_type: u32 = rd_u32(buf, sh_off + SHDR_TYPE);
let sh_name: u32 = rd_u32(buf, sh_off + SHDR_NAME);
let nm: *u8 = shstr + (sh_name: u64);
if (sh_type == SHT_PROGBITS: u32) {
if (cstr_eq(nm, ".text")) { idx_text = i: i32; };
};
if (sh_type == SHT_SYMTAB: u32) { idx_symtab = i: i32; };
if (sh_type == SHT_RELA: u32) {
if (cstr_eq(nm, ".rela.text")) { idx_rela = i: i32; };
};
i += 1u32;
};
if (idx_text < 0) {
os.write(2, "6l: missing .text\n".ptr, 18u64);
return -1;
};
if (idx_symtab < 0) {
os.write(2, "6l: missing .symtab\n".ptr, 20u64);
return -1;
};
let text_sh: u64 = shoff + (idx_text: u64) * SHDR_SIZE;
let text_off: u64 = rd_u64(buf, text_sh + SHDR_OFFSET);
let text_size: u64 = rd_u64(buf, text_sh + SHDR_SIZE_F);
let sym_sh: u64 = shoff + (idx_symtab: u64) * SHDR_SIZE;
let sym_off: u64 = rd_u64(buf, sym_sh + SHDR_OFFSET);
let sym_size: u64 = rd_u64(buf, sym_sh + SHDR_SIZE_F);
let sym_link: u32 = rd_u32(buf, sym_sh + SHDR_LINK);
let nsyms: u64 = sym_size / SYM_SIZE;
let str_sh: u64 = shoff + (sym_link: u64) * SHDR_SIZE;
let str_off: u64 = rd_u64(buf, str_sh + SHDR_OFFSET);
let strtab: *u8 = buf + str_off;
// Track this object.
let ob: *lobj = amalloc(l.a, 64u64): *lobj;
ob.path = cstr_to_str(l.a, path_cs);
ob.buf = buf;
ob.len = len;
ob.text_off = l.textlen;
ob.text_size = text_size;
ob.onext = l.objs;
l.objs = ob;
// Append .text bytes to the combined image.
emit_text(l, buf + text_off, text_size);
// 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 sym_p: u64 = sym_off + si * SYM_SIZE;
let st_name: u32 = rd_u32(buf, sym_p + SYM_NAME);
let st_shndx: u16 = rd_u16(buf, sym_p + SYM_SHNDX);
let st_value: u64 = rd_u64(buf, sym_p + SYM_VALUE);
let nm_p: *u8 = strtab + (st_name: u64);
if (nm_p[0u64] != 0u8) {
let nm: str = cstr_to_str(l.a, nm_p);
let gs: *lsym = l_intern(l, nm);
if (st_shndx != 0u16) {
if ((st_shndx: i32) == idx_text) {
if (gs.defined != 0) {
os.write(2, "6l: duplicate symbol\n".ptr, 21u64);
l.errs += 1;
} else {
gs.defined = 1;
gs.owner = ob;
gs.idx_in_owner = si: i32;
gs.val = ob.text_off + st_value;
};
};
};
};
si += 1u64;
};
// Per-object relocation collection.
if (idx_rela >= 0) {
let rela_sh: u64 = shoff + (idx_rela: u64) * SHDR_SIZE;
let rela_off: u64 = rd_u64(buf, rela_sh + SHDR_OFFSET);
let rela_size: u64 = rd_u64(buf, rela_sh + SHDR_SIZE_F);
let nrel: u64 = rela_size / RELA_SIZE;
let ri: u64 = 0u64;
for (ri < nrel) {
let rp: u64 = rela_off + ri * RELA_SIZE;
let r_off: u64 = rd_u64(buf, rp + RELA_OFFSET);
let r_info: u64 = rd_u64(buf, rp + RELA_INFO);
let r_addend: u64 = rd_u64(buf, rp + RELA_ADDEND);
let r_sym_idx: u32 = (r_info >> 32u64): u32;
let r_kind: i32 = ((r_info & 4294967295u64): u32): i32;
let nr: *lrel = amalloc(l.a, 48u64): *lrel;
nr.off = ob.text_off + r_off;
nr.kind = r_kind;
nr.addend = r_addend: i64;
// Look up the referenced sym by name (re-walk symtab).
if ((r_sym_idx: u64) < nsyms) {
let s_p: u64 = sym_off + (r_sym_idx: u64) * SYM_SIZE;
let s_name: u32 = rd_u32(buf, s_p + SYM_NAME);
let s_nm: *u8 = strtab + (s_name: u64);
if (s_nm[0u64] != 0u8) {
let nm: str = cstr_to_str(l.a, s_nm);
nr.sym = l_intern(l, nm);
};
};
nr.rnext = l.rels;
l.rels = nr;
ri += 1u64;
};
};
return 0;
};
// selfhost/cmd/6l/pass.ww — port of cmd/6l/pass.c.
//
// Resolution + relocation. l_resolve flags every undefined symbol
// referenced by a relocation. l_relocate walks the rel list and
// patches the .text bytes in place once the final virtual base is
// known. Supported relocation kinds: PC32 (=2), PLT32 (=4); both
// are 32-bit PC-relative displacements (PLT32 == PC32 for static).
use os;
use sym;
def R_X86_64_PC32: i32 = 2;
def R_X86_64_PLT32: i32 = 4;
export fn l_resolve(l: *lnk) i32 = {
let r: *lrel = l.rels;
for (r != nil) {
if (r.sym != nil) {
if (r.sym.defined == 0) {
os.write(2, "6l: undefined reference to '".ptr, 28u64);
let nm: str = r.sym.name;
os.write(2, nm.ptr, nm.len: u64);
os.write(2, "'\n".ptr, 2u64);
l.errs += 1;
};
};
r = r.rnext;
};
return l.errs;
};
fn patch_u32(p: *u8, v: u32) void = {
p[0] = (v & 255u32): u8;
p[1] = ((v >> 8u32) & 255u32): u8;
p[2] = ((v >> 16u32) & 255u32): u8;
p[3] = ((v >> 24u32) & 255u32): u8;
};
export fn l_relocate(l: *lnk, base: u64) i32 = {
let r: *lrel = l.rels;
for (r != nil) {
if (r.sym != nil) {
if (r.sym.defined != 0) {
let k: i32 = r.kind;
if (k == R_X86_64_PC32) {
let site: u64 = base + r.off;
let target: i64 = (base + r.sym.val): i64;
let rel: i64 = (target - site: i64) + r.addend;
patch_u32(l.text + r.off, rel: u32);
} else { if (k == R_X86_64_PLT32) {
let site: u64 = base + r.off;
let target: i64 = (base + r.sym.val): i64;
let rel: i64 = (target - site: i64) + r.addend;
patch_u32(l.text + r.off, rel: u32);
} else {
os.write(2, "6l: unsupported reloc kind\n".ptr, 27u64);
l.errs += 1;
};};
};
};
r = r.rnext;
};
return l.errs;
};
// selfhost/cmd/6l/out.ww — port of cmd/6l/out.c.
//
// Emit a static ELF64 executable. File layout (per the C original):
// [0..64) Ehdr
// [64..120) Phdr (one PT_LOAD)
// [120..0x1000) zero pad
// [0x1000..) .text bytes
// Single PT_LOAD covers the whole file, R+X. No interpreter, no .bss.
use os;
use sym;
def ET_EXEC: u16 = 2u16;
def EM_X86_64_W: u16 = 62u16;
def EV_CURRENT: u32 = 1u32;
def ELFCLASS64: u8 = 2u8;
def ELFDATA2LSB: u8 = 1u8;
def PT_LOAD: u32 = 1u32;
def PF_X: u32 = 1u32;
def PF_R: u32 = 4u32;
def TEXT_OFF: u64 = 4096u64; // 0x1000
// ---- little-endian byte writers ----------------------------------------
fn wr_u16(buf: *u8, off: u64, v: u16) void = {
buf[off] = (v & 255u16): u8;
buf[off + 1u64] = ((v >> 8u16) & 255u16): u8;
};
fn wr_u32(buf: *u8, off: u64, v: u32) void = {
buf[off] = (v & 255u32): u8;
buf[off + 1u64] = ((v >> 8u32) & 255u32): u8;
buf[off + 2u64] = ((v >> 16u32) & 255u32): u8;
buf[off + 3u64] = ((v >> 24u32) & 255u32): u8;
};
fn wr_u64(buf: *u8, off: u64, v: u64) void = {
wr_u32(buf, off, (v & 4294967295u64): u32);
wr_u32(buf, off + 4u64, ((v >> 32u64) & 4294967295u64): u32);
};
// ---- emit ---------------------------------------------------------------
export fn l_emit_elf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = {
let filesz: u64 = TEXT_OFF + l.textlen;
// One contiguous header buffer covering [0..0x1000), then .text.
let hdr: *u8 = os.alloc(TEXT_OFF): *u8; // zero-initialised by mmap
// --- Ehdr (64 bytes) ---
hdr[0u64] = 127u8; // 0x7f
hdr[1u64] = 69u8; // 'E'
hdr[2u64] = 76u8; // 'L'
hdr[3u64] = 70u8; // 'F'
hdr[4u64] = ELFCLASS64;
hdr[5u64] = ELFDATA2LSB;
hdr[6u64] = EV_CURRENT: u8;
wr_u16(hdr, 16u64, ET_EXEC); // e_type
wr_u16(hdr, 18u64, EM_X86_64_W); // e_machine
wr_u32(hdr, 20u64, EV_CURRENT); // e_version
wr_u64(hdr, 24u64, entry); // e_entry
wr_u64(hdr, 32u64, 64u64); // e_phoff = sizeof(Ehdr)
wr_u64(hdr, 40u64, 0u64); // e_shoff
wr_u32(hdr, 48u64, 0u32); // e_flags
wr_u16(hdr, 52u64, 64u16); // e_ehsize
wr_u16(hdr, 54u64, 56u16); // e_phentsize
wr_u16(hdr, 56u64, 1u16); // e_phnum
wr_u16(hdr, 58u64, 0u16); // e_shentsize
wr_u16(hdr, 60u64, 0u16); // e_shnum
wr_u16(hdr, 62u64, 0u16); // e_shstrndx
// --- Phdr (56 bytes) at offset 64 ---
wr_u32(hdr, 64u64, PT_LOAD); // p_type
wr_u32(hdr, 68u64, PF_R | PF_X); // p_flags
wr_u64(hdr, 72u64, 0u64); // p_offset
wr_u64(hdr, 80u64, base); // p_vaddr
wr_u64(hdr, 88u64, base); // p_paddr
wr_u64(hdr, 96u64, filesz); // p_filesz
wr_u64(hdr, 104u64, filesz); // p_memsz
wr_u64(hdr, 112u64, TEXT_OFF); // p_align
// Write [0..0x1000) then .text.
let n1: i64 = os.writefull(fd, hdr, TEXT_OFF);
if (n1 != TEXT_OFF: i64) { return -1; };
if (l.textlen > 0u64) {
let n2: i64 = os.writefull(fd, l.text, l.textlen);
if (n2 != l.textlen: i64) { return -1; };
};
return 0;
};
// selfhost/cmd/6l/main.ww — port of cmd/6l/main.c.
//
// 6l = amd64 static linker. Reads relocatable ELF .o files and
// SysV `ar` archives, resolves symbols, applies relocations, writes
// a static ELF executable.
//
// 6l_ww -o out file1.o file2.o libwwrt.a ...
use os;
use mem;
use sym;
use obj;
use pass;
use out;
def BASE: u64 = 4194304u64; // 0x400000
def CODE_VA_OFF: u64 = 4096u64; // .text starts at base + 0x1000
// Linker context lives in main's frame; arena gets passed in.
fn make_lnk(a: *arena) *lnk = {
let l: *lnk = amalloc(a, 96u64): *lnk;
l.a = a;
return l;
};
fn streq_cs(a: *u8, lit: str) bool = {
let n: u64 = lit.len: u64;
let i: u64 = 0u64;
for (i < n) {
let li: i32 = i: i32;
if (a[i] != lit[li]) { return false; };
i += 1u64;
};
if (a[i] != 0u8) { return false; };
return true;
};
export fn main(argc: i32, argv: **u8) i32 = {
let out_path: *u8 = nil;
// Inputs: store as **u8 (heap'd from a fixed-size buffer).
let max_inputs: i32 = 64;
let inputs: **u8 = os.alloc((max_inputs: u64) * 8u64): **u8;
let ninputs: i32 = 0;
let i: i32 = 1;
for (i < argc) {
let a: *u8 = argv[i];
if (streq_cs(a, "-o")) {
i += 1;
if (i >= argc) {
os.write(2, "6l: -o requires argument\n".ptr, 25u64);
return 2;
};
out_path = argv[i];
} else { if (a[0u64] == 45u8) {
os.write(2, "6l: unknown flag\n".ptr, 17u64);
return 2;
} else {
if (ninputs >= max_inputs) {
os.write(2, "6l: too many inputs\n".ptr, 20u64);
return 2;
};
inputs[ninputs] = a;
ninputs += 1;
}; };
i += 1;
};
if (out_path == nil) {
os.write(2, "usage: 6l_ww -o exe file1.o [file2.o...]\n".ptr, 41u64);
return 2;
};
if (ninputs == 0) {
os.write(2, "6l: no inputs\n".ptr, 14u64);
return 2;
};
let a: *arena = newarena();
let l: *lnk = make_lnk(a);
// Seed _start so a libwwrt-style start.o is recognised as wanted.
l_intern(l, "_start");
let k: i32 = 0;
for (k < ninputs) {
if (l_load(l, inputs[k]) != 0) {
return 1;
};
k += 1;
};
if (l_resolve(l) != 0) { return 1; };
if (l_relocate(l, BASE + CODE_VA_OFF) != 0) { return 1; };
let entry_sym: *lsym = l_lookup(l, "_start");
if (entry_sym == nil) { entry_sym = l_lookup(l, "main"); }
else { if (entry_sym.defined == 0) { entry_sym = l_lookup(l, "main"); }; };
if (entry_sym == nil) {
os.write(2, "6l: no _start or main symbol\n".ptr, 29u64);
return 1;
};
if (entry_sym.defined == 0) {
os.write(2, "6l: no _start or main symbol\n".ptr, 29u64);
return 1;
};
// Open output: O_WRONLY|O_CREAT|O_TRUNC, mode 0755.
let flags: i32 = os.O_WRONLY | os.O_CREAT | os.O_TRUNC;
let fd: i32 = os.open(out_path, flags, 493i32); // 0o755
if (fd < 0) {
os.write(2, "6l: cannot open output\n".ptr, 23u64);
return 1;
};
let entry_va: u64 = BASE + CODE_VA_OFF + entry_sym.val;
let rc: i32 = l_emit_elf(l, fd, BASE, entry_va);
os.close(fd);
return rc;
};