ww: rename toolchain to w-prefix + hare-style build/run/test driver

Plan 9-style w-prefix on the per-arch tools, disambiguating from the
real Plan 9 6c/6a/6l in ref/plan9front/:

    cmd/wwc/      → cmd/wcc/        libwwc.a → libwcc.a
    cmd/6{c,a,l}  → cmd/w6{c,a,l}   binary names too
    test/wwc/     → test/wcc/       6 test files w/ w6 prefix
    selfhost/cmd  mirror in lockstep
    bootstrap/amd64/{w6c,w6a,w6l}   snapshot binaries (gitignored)
    WW_6{C,A,L}   → WW_W6{C,A,L}    env-var overrides

Plan 9 source-tree refs ("Plan 9 6c shape", ref/plan9front/, etc.)
preserved. Hare-style driver, both C and ww sides:

    ww test [path]   discover *_test.ww in a directory module, run
                     each; single-file mode for `ww test foo.ww`
    Module-by-name   `ww build foo` resolves to foo.ww or foo/foo.ww
                     via search path (cwd : -I dirs : $WW_LIB)
    Default-to-cwd   `ww build` / `ww test` build the cwd module
    Run pass-through `ww run path arg1 arg2` reaches the program

lib/os: getcwd (79) and getdents64 (217) syscalls power `.` resolution
and directory enumeration on the ww side.

Makefile: wwstage tool deps now include lib/os/os.ww (+ lib/strconv
for wwdump_ww) so lib/* edits force their rebuild instead of leaving
stale binaries — surfaced when test 995 first failed against a stale
w6c_ww built before the lib/os additions.

Test 993 byte-identical parity gate (C-side ww vs ww-side ww_ww on a
build corpus) stays green; all 19 tests pass.
This commit is contained in:
2026-05-11 13:49:27 +09:00
parent e217cd32d1
commit 2c33228b7e
96 changed files with 2129 additions and 973 deletions

262
cmd/w6l/dyn.c Normal file
View File

@@ -0,0 +1,262 @@
/*
* dyn.c — load a shared object (ET_DYN) so the linker knows which
* symbols it exports and which DT_NEEDED entry to record. We do not
* pull bytes from the .so; the dynamic loader maps it at runtime.
*
* Each call appends one Lso to lnk->sos. `l_so_provides` answers
* "does this .so export the named symbol?" — l_resolve uses that to
* promote unresolved references to dynamic.
*/
#include "l.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ET_DYN 3
#define SHT_DYNAMIC 6
#define SHT_DYNSYM 11
/* GNU extensions, sh_type values. */
#define SHT_GNU_VERDEF 0x6ffffffd
#define SHT_GNU_VERNEED 0x6ffffffe
#define SHT_GNU_VERSYM 0x6fffffff
#define DT_NULL 0
#define DT_SONAME 14
#define DT_STRTAB 5
/* Versym special values: 0 = local, 1 = base/global. */
#define VER_NDX_LOCAL 0
#define VER_NDX_GLOBAL 1
#define VER_FLG_BASE 1
#define VERSYM_HIDDEN 0x8000
#define VERSYM_VERSION 0x7fff
#pragma pack(push, 1)
typedef struct {
u8 e_ident[16];
u16 e_type, e_machine;
u32 e_version;
u64 e_entry, e_phoff, e_shoff;
u32 e_flags;
u16 e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx;
} Ehdr;
typedef struct {
u32 sh_name, sh_type;
u64 sh_flags, sh_addr, sh_offset, sh_size;
u32 sh_link, sh_info;
u64 sh_addralign, sh_entsize;
} Shdr;
typedef struct {
u32 st_name;
u8 st_info, st_other;
u16 st_shndx;
u64 st_value, st_size;
} Sym64;
typedef struct {
i64 d_tag;
u64 d_val;
} Dyn64;
typedef struct {
u16 vd_version;
u16 vd_flags;
u16 vd_ndx;
u16 vd_cnt;
u32 vd_hash;
u32 vd_aux;
u32 vd_next;
} Verdef;
typedef struct {
u32 vda_name;
u32 vda_next;
} Verdaux;
#pragma pack(pop)
/* ELF binding values. STB_GLOBAL = 1, STB_WEAK = 2. */
#define ST_BIND(i) ((i) >> 4)
int
l_load_so(Lnk *l, const char *path)
{
u8 *buf;
u64 len;
if (l_read_all(path, &buf, &len) < 0) {
fprintf(stderr, "w6l: %s: cannot read\n", path);
return -1;
}
if (len < sizeof(Ehdr)) {
fprintf(stderr, "w6l: %s: short ELF\n", path);
free(buf);
return -1;
}
Ehdr *eh = (Ehdr *)buf;
if (memcmp(eh->e_ident, "\x7f""ELF", 4) != 0
|| eh->e_ident[4] != 2
|| eh->e_machine != 62 /* EM_X86_64 */
|| eh->e_type != ET_DYN) {
fprintf(stderr, "w6l: %s: not an amd64 ET_DYN\n", path);
free(buf);
return -1;
}
if (eh->e_shoff == 0 || eh->e_shnum == 0) {
fprintf(stderr, "w6l: %s: stripped .so unsupported\n", path);
free(buf);
return -1;
}
Shdr *sh = (Shdr *)(buf + eh->e_shoff);
int idx_dynsym = -1, idx_dynamic = -1;
int idx_versym = -1, idx_verdef = -1;
for (u16 i = 0; i < eh->e_shnum; i++) {
if (sh[i].sh_type == SHT_DYNSYM) idx_dynsym = i;
if (sh[i].sh_type == SHT_DYNAMIC) idx_dynamic = i;
if (sh[i].sh_type == SHT_GNU_VERSYM) idx_versym = i;
if (sh[i].sh_type == SHT_GNU_VERDEF) idx_verdef = i;
}
if (idx_dynsym < 0) {
fprintf(stderr, "w6l: %s: no .dynsym\n", path);
free(buf);
return -1;
}
int idx_dynstr = sh[idx_dynsym].sh_link;
const char *str = (const char *)(buf + sh[idx_dynstr].sh_offset);
Sym64 *syms = (Sym64 *)(buf + sh[idx_dynsym].sh_offset);
u64 nsyms = sh[idx_dynsym].sh_size / sizeof(Sym64);
/* SONAME: the .dynamic section's strings live in the section pointed
* at by its sh_link (not necessarily .dynstr — though usually). */
const char *soname = NULL;
if (idx_dynamic >= 0) {
int idx_dstr = sh[idx_dynamic].sh_link;
const char *dstr = (const char *)(buf + sh[idx_dstr].sh_offset);
Dyn64 *d = (Dyn64 *)(buf + sh[idx_dynamic].sh_offset);
u64 nd = sh[idx_dynamic].sh_size / sizeof(Dyn64);
for (u64 i = 0; i < nd; i++) {
if (d[i].d_tag == DT_NULL) break;
if (d[i].d_tag == DT_SONAME) {
soname = dstr + d[i].d_val;
break;
}
}
}
if (soname == NULL) {
const char *bn = strrchr(path, '/');
soname = bn ? bn + 1 : path;
}
/* Build verdef-index → version-name table. The version name is in
* the first Verdaux (the rest are predecessor names — version
* inheritance for stable ABI within a release line). For our
* purposes only the leading name matters. */
const char **verdef_names = NULL;
int verdef_max = 0;
if (idx_verdef >= 0) {
const u8 *vbase = buf + sh[idx_verdef].sh_offset;
const char *vstr = (const char *)
(buf + sh[sh[idx_verdef].sh_link].sh_offset);
/* First pass: discover max ndx so we can size the table. */
u64 vd_off = 0;
while (vd_off < sh[idx_verdef].sh_size) {
Verdef *vd = (Verdef *)(vbase + vd_off);
if ((int)vd->vd_ndx > verdef_max)
verdef_max = vd->vd_ndx;
if (vd->vd_next == 0) break;
vd_off += vd->vd_next;
}
verdef_names = calloc((size_t)verdef_max + 1,
sizeof *verdef_names);
vd_off = 0;
while (vd_off < sh[idx_verdef].sh_size) {
Verdef *vd = (Verdef *)(vbase + vd_off);
Verdaux *va = (Verdaux *)((u8 *)vd + vd->vd_aux);
verdef_names[vd->vd_ndx] = vstr + va->vda_name;
if (vd->vd_next == 0) break;
vd_off += vd->vd_next;
}
}
/* Versym is one u16 per .dynsym entry. */
const u16 *versym = NULL;
if (idx_versym >= 0)
versym = (const u16 *)(buf + sh[idx_versym].sh_offset);
Lso *so = calloc(1, sizeof *so);
so->path = strdup(path);
so->soname = strdup(soname);
so->exports = calloc((size_t)nsyms + 1, sizeof *so->exports);
so->versions = calloc((size_t)nsyms + 1, sizeof *so->versions);
int n = 0;
for (u64 i = 1; i < nsyms; i++) {
if (syms[i].st_shndx == 0) continue; /* SHN_UNDEF */
u8 b = ST_BIND(syms[i].st_info);
if (b != 1 && b != 2) continue; /* GLOBAL or WEAK */
const char *nm = str + syms[i].st_name;
if (nm[0] == '\0') continue;
/* Skip non-default versions: when a name has multiple version
* definitions, the loader binds an unversioned reference to
* the one whose Versym entry has the hidden bit clear. */
const char *vername = NULL;
if (versym != NULL) {
u16 v = versym[i];
if (v & VERSYM_HIDDEN) continue; /* non-default */
u16 vidx = v & VERSYM_VERSION;
if (vidx == VER_NDX_LOCAL) continue; /* not exported */
if (vidx == VER_NDX_GLOBAL) {
vername = NULL; /* unversioned */
} else if (verdef_names != NULL
&& (int)vidx <= verdef_max
&& verdef_names[vidx] != NULL) {
/* index 1 in glibc's Verdef is the SONAME with
* VER_FLG_BASE — we skip its export entries
* as a side effect of vidx==1 mapping to the
* BASE name (e.g. "libc.so.6"), which never
* appears as a reference target. Treat any
* lookup that lands on the BASE entry as
* unversioned. */
if (vidx == 1)
vername = NULL;
else
vername = verdef_names[vidx];
}
}
so->exports[n] = strdup(nm);
so->versions[n] = vername ? strdup(vername) : NULL;
n++;
}
so->exports[n] = NULL;
so->versions[n] = NULL;
if (verdef_names != NULL) free(verdef_names);
so->next = l->sos;
l->sos = so;
free(buf);
return 0;
}
int
l_so_provides(Lso *so, const char *name)
{
const char *v;
return l_so_provides_v(so, name, &v);
}
int
l_so_provides_v(Lso *so, const char *name, const char **out_version)
{
if (so == NULL || so->exports == NULL) return 0;
for (int i = 0; so->exports[i]; i++) {
if (strcmp(so->exports[i], name) != 0) continue;
*out_version = so->versions ? so->versions[i] : NULL;
return 1;
}
return 0;
}

644
cmd/w6l/dynout.c Normal file
View File

@@ -0,0 +1,644 @@
/*
* dynout.c — emit a dynamic-linked ELF executable.
*
* The shape we produce is the simplest valid one: PT_INTERP +
* PT_DYNAMIC + DT_BIND_NOW so the loader resolves every PLT slot at
* startup (no lazy binding, no PLT0 trampoline). Symbol versioning
* is omitted; modern glibc tolerates unversioned references by
* binding to each symbol's "default" version. SysV .hash, not
* .gnu.hash. Non-PIE, fixed base.
*
* File layout:
* [0] Ehdr
* [64] Phdrs (PT_LOAD R+X, PT_LOAD R+W, PT_INTERP, PT_DYNAMIC)
* [interp_off] "/lib64/ld-linux-x86-64.so.2\0"
* [dynstr_off] .dynstr
* [dynsym_off] .dynsym
* [hash_off] .hash
* [relaplt_off] .rela.plt
* [pad to 0x1000]
* [text_off] .text
* [plt_off] .plt
* [pad to next page]
* [gotplt_off] .got.plt (writable; mapped by PT_LOAD #2)
* [dynamic_off] .dynamic (writable; covered by PT_DYNAMIC)
*
* Each PLT entry is 8 bytes: `jmpq *(rip+disp)` (6 bytes) + 2 bytes
* pad so the next entry stays naturally aligned.
*/
#include "l.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ELF constants */
#define ET_EXEC 2
#define EM_X86_64 62
#define EV_CURRENT 1
#define ELFCLASS64 2
#define ELFDATA2LSB 1
#define PT_LOAD 1
#define PT_DYNAMIC 2
#define PT_INTERP 3
#define PF_X 1
#define PF_W 2
#define PF_R 4
#define DT_NULL 0
#define DT_NEEDED 1
#define DT_PLTRELSZ 2
#define DT_PLTGOT 3
#define DT_HASH 4
#define DT_STRTAB 5
#define DT_SYMTAB 6
#define DT_STRSZ 10
#define DT_SYMENT 11
#define DT_PLTREL 20
#define DT_RELA 7
#define DT_JMPREL 23
#define DT_BIND_NOW 24
/* GNU extensions for symbol versioning. */
#define DT_VERSYM 0x6ffffff0
#define DT_VERNEED 0x6ffffffe
#define DT_VERNEEDNUM 0x6fffffff
#define VER_NDX_LOCAL 0
#define VER_NDX_GLOBAL 1
#define R_X86_64_PC32 2
#define R_X86_64_PLT32 4
#define R_X86_64_JUMP_SLOT 7
#define STB_GLOBAL 1
#define STT_FUNC 2
#define ST_INFO(b,t) (((b) << 4) | ((t) & 0xf))
#define INTERP "/lib64/ld-linux-x86-64.so.2"
#pragma pack(push, 1)
typedef struct {
u8 e_ident[16];
u16 e_type, e_machine;
u32 e_version;
u64 e_entry, e_phoff, e_shoff;
u32 e_flags;
u16 e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx;
} Ehdr;
typedef struct {
u32 p_type, p_flags;
u64 p_offset, p_vaddr, p_paddr;
u64 p_filesz, p_memsz, p_align;
} Phdr;
typedef struct {
u32 st_name;
u8 st_info, st_other;
u16 st_shndx;
u64 st_value, st_size;
} Sym64;
typedef struct {
u64 r_offset;
u64 r_info;
i64 r_addend;
} Rela64;
typedef struct {
i64 d_tag;
u64 d_val;
} Dyn64;
#pragma pack(pop)
#define ELF64_R_INFO(s,t) (((u64)(s) << 32) | ((u32)(t)))
/* SysV ELF hash (the older format; .gnu.hash is faster but more code). */
static u32
elf_hash(const char *name)
{
u32 h = 0, g;
for (const u8 *s = (const u8 *)name; *s; s++) {
h = (h << 4) + *s;
g = h & 0xf0000000u;
if (g) h ^= g >> 24;
h &= ~g;
}
return h;
}
/* Patch a 4-byte little-endian field in `buf` at offset `off`. */
static void
poke32(u8 *buf, u64 off, u32 v)
{
buf[off + 0] = (u8)(v);
buf[off + 1] = (u8)(v >> 8);
buf[off + 2] = (u8)(v >> 16);
buf[off + 3] = (u8)(v >> 24);
}
#define PLT_STUB_BYTES 8 /* jmpq *disp(%rip) + 2 nop pad */
int
l_emit_dyn_elf(Lnk *l, FILE *f, u64 base, u64 entry)
{
const int N = l->dyn_n;
/* ---- Pass 1: collect dynamic symbol names + .dynstr layout ---- */
/* dynstr layout: [0]='\0', then DT_NEEDED soname strings, then
* one symbol name per dynamic Lsym. We index dyn syms by
* plt_idx (assigned in l_resolve). Build an array sorted by
* plt_idx so we can walk in slot order. */
Lsym **dynsyms = calloc((size_t)N, sizeof *dynsyms);
for (Lsym *s = l->syms; s; s = s->next) {
if (s->is_dyn && s->plt_idx >= 0 && s->plt_idx < N)
dynsyms[s->plt_idx] = s;
}
for (int i = 0; i < N; i++) {
if (dynsyms[i] == NULL) {
fprintf(stderr, "w6l: dynout: no sym for plt_idx %d\n", i);
free(dynsyms);
return 1;
}
}
/* Count Lso's that any dyn sym references; only those need DT_NEEDED. */
int nsos = 0;
for (Lso *so = l->sos; so; so = so->next) {
int used = 0;
for (int i = 0; i < N; i++)
if (dynsyms[i]->dyn_lib == so) { used = 1; break; }
if (used) nsos++;
}
Lso **sos_used = calloc((size_t)nsos, sizeof *sos_used);
{
int idx = 0;
for (Lso *so = l->sos; so; so = so->next) {
int used = 0;
for (int i = 0; i < N; i++)
if (dynsyms[i]->dyn_lib == so) { used = 1; break; }
if (used) sos_used[idx++] = so;
}
}
/* Build .dynstr in a growable buffer. */
u8 *dynstr = NULL;
u64 dynstr_cap = 0, dynstr_len = 0;
#define DSTR_PUT(s) do { \
size_t _n = strlen(s) + 1; \
if (dynstr_len + _n > dynstr_cap) { \
dynstr_cap = dynstr_cap ? dynstr_cap * 2 : 256; \
while (dynstr_cap < dynstr_len + _n) dynstr_cap *= 2; \
dynstr = realloc(dynstr, dynstr_cap); \
} \
memcpy(dynstr + dynstr_len, s, _n); \
dynstr_len += _n; \
} while (0)
DSTR_PUT(""); /* leading null entry */
u32 *soname_str = calloc((size_t)nsos, sizeof *soname_str);
for (int i = 0; i < nsos; i++) {
soname_str[i] = (u32)dynstr_len;
DSTR_PUT(sos_used[i]->soname);
}
u32 *symname_str = calloc((size_t)N, sizeof *symname_str);
for (int i = 0; i < N; i++) {
symname_str[i] = (u32)dynstr_len;
DSTR_PUT(dynsyms[i]->name);
}
/* ---- Versioning: group dyn syms by (lib, version) ----
*
* For every sym whose dyn_version is non-NULL, there's a
* Vernaux record under that lib's Verneed. The vna_other
* value (assigned starting at 2; 1 is reserved for "global,
* unversioned") becomes that sym's .gnu.version entry.
* Unversioned syms get .gnu.version = 1.
*
* vlibs is parallel-indexed with sos_used so we can look
* up the SONAME's dynstr offset directly.
*/
struct vlib_ver { const char *name; u32 dynstr_off; u16 vna_other; };
struct vlib { int sos_idx; int n_versions; struct vlib_ver *versions; };
struct vlib *vlibs = calloc((size_t)nsos, sizeof *vlibs);
int n_vlibs = 0;
for (int i = 0; i < nsos; i++) {
int has = 0;
for (int j = 0; j < N; j++) {
if (dynsyms[j]->dyn_lib == sos_used[i]
&& dynsyms[j]->dyn_version != NULL) {
has = 1; break;
}
}
if (!has) continue;
struct vlib *vl = &vlibs[n_vlibs];
vl->sos_idx = i;
vl->versions = calloc((size_t)N, sizeof *vl->versions);
vl->n_versions = 0;
for (int j = 0; j < N; j++) {
if (dynsyms[j]->dyn_lib != sos_used[i]) continue;
const char *vname = dynsyms[j]->dyn_version;
if (vname == NULL) continue;
int seen = 0;
for (int k = 0; k < vl->n_versions; k++) {
if (strcmp(vl->versions[k].name, vname) == 0) {
seen = 1; break;
}
}
if (!seen) {
vl->versions[vl->n_versions].name = vname;
vl->n_versions++;
}
}
n_vlibs++;
}
/* Assign vna_other indices starting at 2. */
u16 next_vna = 2;
for (int i = 0; i < n_vlibs; i++)
for (int k = 0; k < vlibs[i].n_versions; k++)
vlibs[i].versions[k].vna_other = next_vna++;
/* Add version name strings to .dynstr. */
for (int i = 0; i < n_vlibs; i++) {
for (int k = 0; k < vlibs[i].n_versions; k++) {
vlibs[i].versions[k].dynstr_off = (u32)dynstr_len;
DSTR_PUT(vlibs[i].versions[k].name);
}
}
/* Per-dyn-sym versym index: 1 (global) for unversioned, else
* the matched Vernaux's vna_other. */
u16 *versym_for = calloc((size_t)N, sizeof *versym_for);
for (int j = 0; j < N; j++) {
const char *vname = dynsyms[j]->dyn_version;
if (vname == NULL) { versym_for[j] = VER_NDX_GLOBAL; continue; }
int matched = 0;
for (int i = 0; i < n_vlibs && !matched; i++) {
if (sos_used[vlibs[i].sos_idx] != dynsyms[j]->dyn_lib)
continue;
for (int k = 0; k < vlibs[i].n_versions; k++) {
if (strcmp(vlibs[i].versions[k].name, vname) == 0) {
versym_for[j] = vlibs[i].versions[k].vna_other;
matched = 1; break;
}
}
}
if (!matched) {
fprintf(stderr, "w6l: dynout: unmatched version %s for %s\n",
vname, dynsyms[j]->name);
versym_for[j] = VER_NDX_GLOBAL;
}
}
/* ---- Pass 2: compute byte sizes of every section ---- */
const u64 ehdr_sz = sizeof(Ehdr);
const int n_phdrs = 4;
const u64 phdr_sz = (u64)n_phdrs * sizeof(Phdr);
const u64 interp_sz = strlen(INTERP) + 1;
/* nsyms = 1 (undef at index 0) + N */
const int nsyms_total = 1 + N;
const u64 dynsym_sz = (u64)nsyms_total * sizeof(Sym64);
const u64 dynstr_sz = dynstr_len;
/* SysV hash: nbuckets + nchain + buckets[] + chain[]. We use one
* bucket; loader scans the whole chain. Cheap to compute, easy to
* validate. */
const u32 nbuckets = 1;
const u32 nchain = (u32)nsyms_total;
const u64 hash_sz = (2 + nbuckets + nchain) * 4;
const u64 relaplt_sz = (u64)N * sizeof(Rela64);
const u64 plt_sz = (u64)N * PLT_STUB_BYTES;
const u64 gotplt_sz = (3 + (u64)N) * 8;
/* .gnu.version: one Elf64_Half per .dynsym entry. */
const u64 versym_sz = (u64)nsyms_total * 2;
/* .gnu.version_r: per lib, 16-byte Verneed plus 16-byte Vernaux
* for each version under it. */
u64 verneed_sz = 0;
for (int i = 0; i < n_vlibs; i++)
verneed_sz += 16 + 16 * (u64)vlibs[i].n_versions;
/* dynamic entries: NEEDED*nsos, HASH, STRTAB, SYMTAB, STRSZ, SYMENT,
* PLTGOT, PLTRELSZ, PLTREL, JMPREL, BIND_NOW, [VERSYM, VERNEED,
* VERNEEDNUM], NULL. The version trio is conditional on having any
* versioned references. */
const int with_ver = (n_vlibs > 0);
const u64 ndyn = (u64)nsos + 11 + (with_ver ? 3 : 0);
const u64 dynamic_sz = ndyn * sizeof(Dyn64);
/* ---- Pass 3: assign file offsets and virtual addresses ----
* Everything from the Ehdr through .text+.plt is in the R+X
* load segment at base+0..text_end. .got.plt and .dynamic land
* in the R+W segment at the next page boundary. */
u64 off = ehdr_sz + phdr_sz;
const u64 interp_off = off; off += interp_sz;
off = (off + 7) & ~(u64)7;
const u64 dynstr_off = off; off += dynstr_sz;
off = (off + 7) & ~(u64)7;
const u64 dynsym_off = off; off += dynsym_sz;
const u64 hash_off = off; off += hash_sz;
off = (off + 1) & ~(u64)1;
const u64 versym_off = off; off += versym_sz;
off = (off + 3) & ~(u64)3;
const u64 verneed_off = off; off += verneed_sz;
off = (off + 7) & ~(u64)7;
const u64 relaplt_off = off; off += relaplt_sz;
/* Pad to 0x1000 so .text is page-aligned (matters for the loader
* mapping our R+X PT_LOAD). */
const u64 page = 0x1000;
const u64 text_off = (off + page - 1) & ~(page - 1);
const u64 plt_off = text_off + l->textlen;
const u64 rx_end = plt_off + plt_sz;
/* Page-align the writable segment. We skip a page of file bytes;
* the data lands at file offset gotplt_off, vaddr at base+gotplt_va. */
const u64 gotplt_off = (rx_end + page - 1) & ~(page - 1);
const u64 dynamic_off = gotplt_off + gotplt_sz;
const u64 file_end = dynamic_off + dynamic_sz;
/* Virtual addresses mirror file offsets within their segment.
* The R+W segment in particular needs vaddr = base + gotplt_off
* so file offset and vaddr modulo page agree (loader requirement). */
const u64 interp_va = base + interp_off;
const u64 dynstr_va = base + dynstr_off;
const u64 dynsym_va = base + dynsym_off;
const u64 hash_va = base + hash_off;
const u64 versym_va = base + versym_off;
const u64 verneed_va = base + verneed_off;
const u64 relaplt_va = base + relaplt_off;
const u64 text_va = base + text_off;
const u64 plt_va = base + plt_off;
const u64 gotplt_va = base + gotplt_off;
const u64 dynamic_va = base + dynamic_off;
(void)dynstr_va; (void)hash_va; (void)plt_va;
/* ---- Pass 4: build each section into a buffer ---- */
/* .dynsym */
Sym64 *dynsym = calloc((size_t)nsyms_total, sizeof *dynsym);
for (int i = 0; i < N; i++) {
Sym64 *e = &dynsym[1 + i];
e->st_name = symname_str[i];
e->st_info = ST_INFO(STB_GLOBAL, STT_FUNC);
e->st_other = 0;
e->st_shndx = 0; /* SHN_UNDEF */
e->st_value = 0;
e->st_size = 0;
dynsyms[i]->dynsym_idx = 1 + i;
}
/* .hash (SysV format, 1 bucket). */
u32 *hash = calloc(2 + nbuckets + nchain, 4);
hash[0] = nbuckets;
hash[1] = nchain;
/* buckets[0] = first entry that lives in this bucket; we put
* everything in bucket 0, so the bucket head is symbol 1. */
hash[2] = nsyms_total > 1 ? 1 : 0;
/* chain[i] = next sym in the bucket. Last one terminates with 0. */
for (int i = 1; i < nsyms_total; i++) {
u32 next = (i + 1 < nsyms_total) ? (u32)(i + 1) : 0;
hash[2 + nbuckets + i] = next;
}
/* elf_hash is also used by .gnu.version_r for vna_hash below. */
/* .rela.plt */
Rela64 *relaplt = calloc((size_t)N, sizeof *relaplt);
for (int i = 0; i < N; i++) {
relaplt[i].r_offset = gotplt_va + (3 + (u64)i) * 8;
relaplt[i].r_info = ELF64_R_INFO(1 + i, R_X86_64_JUMP_SLOT);
relaplt[i].r_addend = 0;
}
/* .gnu.version: u16 per .dynsym entry. [0] = LOCAL, [1+i] = the
* versym index we computed for dyn sym i. */
u16 *versym = calloc((size_t)nsyms_total, 2);
versym[0] = VER_NDX_LOCAL;
for (int i = 0; i < N; i++)
versym[1 + i] = versym_for[i];
/* .gnu.version_r: chain of Verneed records, one per versioned lib,
* each with a chain of Vernaux records, one per version under it.
* We write directly into a u8 buffer with little-endian poke
* helpers to avoid alignment concerns. */
u8 *verneed = NULL;
if (verneed_sz > 0) {
verneed = calloc((size_t)verneed_sz, 1);
u64 vnoff = 0;
for (int i = 0; i < n_vlibs; i++) {
struct vlib *vl = &vlibs[i];
u64 vn_start = vnoff;
/* Verneed header (16 bytes). */
u8 *vn = verneed + vnoff;
/* vn_version = 1, vn_cnt = nversions */
vn[0] = 1; vn[1] = 0;
vn[2] = (u8)(vl->n_versions);
vn[3] = (u8)(vl->n_versions >> 8);
poke32(vn, 4, soname_str[vl->sos_idx]); /* vn_file */
poke32(vn, 8, 16); /* vn_aux */
/* vn_next set after we know the vernaux count */
vnoff += 16;
for (int k = 0; k < vl->n_versions; k++) {
u8 *va = verneed + vnoff;
poke32(va, 0, elf_hash(vl->versions[k].name));
/* vna_flags = 0 */
va[4] = 0; va[5] = 0;
/* vna_other (versym index) */
va[6] = (u8)(vl->versions[k].vna_other);
va[7] = (u8)(vl->versions[k].vna_other >> 8);
poke32(va, 8, vl->versions[k].dynstr_off);
poke32(va, 12,
(k + 1 < vl->n_versions) ? 16u : 0u);
vnoff += 16;
}
/* Now patch vn_next at vn_start+12. */
poke32(verneed, vn_start + 12,
(i + 1 < n_vlibs)
? (u32)(vnoff - vn_start) : 0u);
}
}
/* .plt — `jmpq *got.plt[3+i](%rip)` per stub.
* Encoding: FF 25 <disp32>. The disp is computed from the address
* of the *next* instruction (RIP after the 6-byte jmp) to the
* GOT slot. */
u8 *plt = calloc((size_t)plt_sz, 1);
for (int i = 0; i < N; i++) {
u64 stub_va = plt_va + (u64)i * PLT_STUB_BYTES;
u64 next_ip = stub_va + 6;
u64 slot_va = gotplt_va + (3 + (u64)i) * 8;
i64 disp = (i64)slot_va - (i64)next_ip;
u8 *p = plt + (u64)i * PLT_STUB_BYTES;
p[0] = 0xff;
p[1] = 0x25;
poke32(p, 2, (u32)(i32)disp);
/* p[6], p[7] left as zero — pad. */
}
/* .got.plt — first three slots are reserved.
* [0] = address of .dynamic (loader reads this).
* [1] = link_map * (loader writes at startup).
* [2] = dl_runtime_resolve (loader writes; unused with BIND_NOW). */
u8 *gotplt = calloc((size_t)gotplt_sz, 1);
{
u64 v = dynamic_va;
for (int b = 0; b < 8; b++) gotplt[b] = (u8)(v >> (b * 8));
}
/* [3..3+N-1] left zero; loader fills via R_X86_64_JUMP_SLOT. */
/* .dynamic */
Dyn64 *dynamic = calloc((size_t)ndyn, sizeof *dynamic);
{
int k = 0;
for (int i = 0; i < nsos; i++) {
dynamic[k].d_tag = DT_NEEDED;
dynamic[k].d_val = soname_str[i];
k++;
}
dynamic[k].d_tag = DT_HASH; dynamic[k].d_val = hash_va; k++;
dynamic[k].d_tag = DT_STRTAB; dynamic[k].d_val = dynstr_va; k++;
dynamic[k].d_tag = DT_SYMTAB; dynamic[k].d_val = dynsym_va; k++;
dynamic[k].d_tag = DT_STRSZ; dynamic[k].d_val = dynstr_sz; k++;
dynamic[k].d_tag = DT_SYMENT; dynamic[k].d_val = sizeof(Sym64); k++;
dynamic[k].d_tag = DT_PLTGOT; dynamic[k].d_val = gotplt_va; k++;
dynamic[k].d_tag = DT_PLTRELSZ; dynamic[k].d_val = relaplt_sz; k++;
dynamic[k].d_tag = DT_PLTREL; dynamic[k].d_val = DT_RELA; k++;
dynamic[k].d_tag = DT_JMPREL; dynamic[k].d_val = relaplt_va; k++;
dynamic[k].d_tag = DT_BIND_NOW; dynamic[k].d_val = 0; k++;
if (with_ver) {
dynamic[k].d_tag = DT_VERSYM;
dynamic[k].d_val = versym_va;
k++;
dynamic[k].d_tag = DT_VERNEED;
dynamic[k].d_val = verneed_va;
k++;
dynamic[k].d_tag = DT_VERNEEDNUM;
dynamic[k].d_val = (u64)n_vlibs;
k++;
}
dynamic[k].d_tag = DT_NULL; dynamic[k].d_val = 0; k++;
if ((u64)k != ndyn) {
fprintf(stderr, "w6l: dynamic entry count mismatch\n");
return 1;
}
}
/* ---- Pass 5: patch .text relocations targeting dynamic syms ---
* The site is the existing PC32/PLT32 displacement field. Target
* is the address of the symbol's PLT stub. */
for (Lrel *r = l->rels; r; r = r->next) {
if (r->sym == NULL || !r->sym->is_dyn) continue;
if (r->kind != R_X86_64_PC32 && r->kind != R_X86_64_PLT32) {
fprintf(stderr, "w6l: dynamic reloc kind %d unsupported\n",
r->kind);
free(dynsyms); free(sos_used); free(soname_str);
free(symname_str); free(dynstr); free(dynsym);
free(hash); free(relaplt); free(plt); free(gotplt);
free(dynamic);
return 1;
}
u64 site = text_va + r->off;
u64 stub = plt_va + (u64)r->sym->plt_idx * PLT_STUB_BYTES;
i64 disp = (i64)stub - (i64)site + r->addend;
poke32(l->text, r->off, (u32)(i32)disp);
}
/* ---- Pass 6: emit ---- */
Ehdr eh = {0};
memcpy(eh.e_ident, "\x7f""ELF", 4);
eh.e_ident[4] = ELFCLASS64;
eh.e_ident[5] = ELFDATA2LSB;
eh.e_ident[6] = EV_CURRENT;
eh.e_type = ET_EXEC;
eh.e_machine = EM_X86_64;
eh.e_version = EV_CURRENT;
eh.e_entry = entry;
eh.e_phoff = ehdr_sz;
eh.e_ehsize = sizeof(Ehdr);
eh.e_phentsize = sizeof(Phdr);
eh.e_phnum = (u16)n_phdrs;
Phdr ph[4] = {0};
/* PT_LOAD #1 — R+X covering everything from Ehdr through .plt. */
ph[0].p_type = PT_LOAD;
ph[0].p_flags = PF_R | PF_X;
ph[0].p_offset = 0;
ph[0].p_vaddr = base;
ph[0].p_paddr = base;
ph[0].p_filesz = rx_end;
ph[0].p_memsz = rx_end;
ph[0].p_align = page;
/* PT_LOAD #2 — R+W covering .got.plt and .dynamic. */
ph[1].p_type = PT_LOAD;
ph[1].p_flags = PF_R | PF_W;
ph[1].p_offset = gotplt_off;
ph[1].p_vaddr = gotplt_va;
ph[1].p_paddr = gotplt_va;
ph[1].p_filesz = file_end - gotplt_off;
ph[1].p_memsz = file_end - gotplt_off;
ph[1].p_align = page;
/* PT_INTERP. */
ph[2].p_type = PT_INTERP;
ph[2].p_flags = PF_R;
ph[2].p_offset = interp_off;
ph[2].p_vaddr = interp_va;
ph[2].p_paddr = interp_va;
ph[2].p_filesz = interp_sz;
ph[2].p_memsz = interp_sz;
ph[2].p_align = 1;
/* PT_DYNAMIC. */
ph[3].p_type = PT_DYNAMIC;
ph[3].p_flags = PF_R | PF_W;
ph[3].p_offset = dynamic_off;
ph[3].p_vaddr = dynamic_va;
ph[3].p_paddr = dynamic_va;
ph[3].p_filesz = dynamic_sz;
ph[3].p_memsz = dynamic_sz;
ph[3].p_align = 8;
fwrite(&eh, 1, sizeof eh, f);
fwrite(ph, 1, sizeof ph, f);
/* helper: pad to absolute offset `to` */
#define PAD_TO(to) do { \
long _here = ftell(f); \
for (long _i = _here; _i < (long)(to); _i++) fputc(0, f); \
} while (0)
PAD_TO(interp_off); fwrite(INTERP, 1, interp_sz, f);
PAD_TO(dynstr_off); fwrite(dynstr, 1, dynstr_sz, f);
PAD_TO(dynsym_off); fwrite(dynsym, 1, dynsym_sz, f);
PAD_TO(hash_off); fwrite(hash, 4, 2 + nbuckets + nchain, f);
PAD_TO(versym_off); fwrite(versym, 2, (size_t)nsyms_total, f);
if (verneed_sz > 0) {
PAD_TO(verneed_off); fwrite(verneed, 1, verneed_sz, f);
}
PAD_TO(relaplt_off); fwrite(relaplt, 1, relaplt_sz, f);
PAD_TO(text_off); fwrite(l->text, 1, l->textlen, f);
PAD_TO(plt_off); fwrite(plt, 1, plt_sz, f);
PAD_TO(gotplt_off); fwrite(gotplt, 1, gotplt_sz, f);
PAD_TO(dynamic_off); fwrite(dynamic, 1, dynamic_sz, f);
for (int i = 0; i < n_vlibs; i++) free(vlibs[i].versions);
free(vlibs); free(versym_for); free(versym);
if (verneed) free(verneed);
free(dynsyms); free(sos_used); free(soname_str);
free(symname_str); free(dynstr); free(dynsym);
free(hash); free(relaplt); free(plt); free(gotplt); free(dynamic);
return 0;
}

104
cmd/w6l/l.h Normal file
View File

@@ -0,0 +1,104 @@
/*
* l.h — w6l-private header. Loads relocatable ELF64 .o files (the
* format produced by w6a) and links them into a static executable.
*
* No archives yet (phase 8). No dynamic linking ever.
*/
#ifndef SIX_L_H
#define SIX_L_H
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
typedef int8_t i8;
typedef int16_t i16;
typedef int32_t i32;
typedef int64_t i64;
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
typedef struct Lsym Lsym;
typedef struct Lrel Lrel;
typedef struct Lobj Lobj;
typedef struct Lso Lso;
typedef struct Lnk Lnk;
struct Lsym {
const char *name;
u64 val; /* offset within combined .text once linked */
int defined; /* 1 if a Lobj defines this symbol */
Lobj *owner;
int idx_in_owner;
/* Dynamic-linking fields. Set by l_resolve when an undefined sym
* is provided by some loaded Lso. Patched-in PLT slot index lets
* the relocator route PC32/PLT32 references through the stub. */
int is_dyn;
Lso *dyn_lib;
const char *dyn_version; /* matched export's version, NULL if none */
int plt_idx; /* 0..dyn_n-1, -1 if no PLT slot */
int dynsym_idx; /* index in emitted .dynsym, -1 otherwise */
Lsym *next;
};
struct Lrel {
u64 off; /* offset within combined .text */
int kind; /* R_X86_64_* */
Lsym *sym;
i64 addend;
Lrel *next;
};
struct Lobj {
const char *path;
u8 *buf; /* mmapped or read-in object bytes */
u64 len;
u64 text_off; /* offset of .text in combined output */
u64 text_size;
Lobj *next;
};
struct Lso {
const char *path; /* full filesystem path used to load */
const char *soname; /* DT_SONAME, or basename if missing */
char **exports; /* NULL-terminated list of GLOBAL/WEAK syms */
char **versions; /* parallel to exports[]; NULL for unversioned,
* else strdup'd version name e.g. "GLIBC_2.2.5" */
Lso *next;
};
struct Lnk {
Lobj *objs;
Lso *sos;
Lsym *syms;
Lrel *rels;
u8 *text; /* combined .text */
u64 textcap, textlen;
int errs;
int dyn_n; /* number of symbols routed through PLT */
};
/* obj.c */
int l_load(Lnk*, const char *path);
int l_read_all(const char *path, u8 **out, u64 *len); /* shared helper */
/* dyn.c */
int l_load_so(Lnk*, const char *path);
int l_so_provides(Lso*, const char *name);
/* l_so_provides_v: same, but also returns the export's version name
* (NULL for unversioned globals) via *out_version on a hit. */
int l_so_provides_v(Lso*, const char *name, const char **out_version);
/* sym.c */
Lsym *l_intern(Lnk*, const char *name);
Lsym *l_lookup(Lnk*, const char *name);
/* pass.c */
int l_resolve(Lnk*);
int l_relocate(Lnk*, u64 base);
/* out.c */
int l_emit_elf(Lnk*, FILE *out, u64 base, u64 entry);
/* dynout.c — emit a dynamic-linked ELF executable. Called by
* l_emit_elf when l->sos is non-empty. */
int l_emit_dyn_elf(Lnk*, FILE *out, u64 base, u64 entry);
#endif

142
cmd/w6l/main.c Normal file
View File

@@ -0,0 +1,142 @@
/*
* w6l — amd64 linker. Reads relocatable ELF .o files (from w6a) plus
* .a archives, resolves, relocates, writes a static ELF executable.
* Dynamic linking against .so files is the next increment; the -L/-l
* flag plumbing here is its first step.
*
* w6l -o out [-L<dir>...] [-l<name>...] file1.o file2.o ...
*
* The first symbol named "_start" defined among the inputs becomes
* the entry point. If none is found, fall back to "main".
*/
#include "l.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* `path` is acceptable iff it's either an archive ("!<arch>\n") or
* an ELF file ("\x7fELF"). Distros often ship lib<name>.so as a GNU
* ld linker script (plain text); we skip those rather than parse the
* GROUP/INPUT directives.
*/
static int
is_linkable(const char *path)
{
FILE *f = fopen(path, "rb");
if (f == NULL) return 0;
u8 magic[8] = {0};
size_t n = fread(magic, 1, 8, f);
fclose(f);
if (n >= 8 && memcmp(magic, "!<arch>\n", 8) == 0) return 1;
if (n >= 4 && memcmp(magic, "\x7f""ELF", 4) == 0) return 1;
return 0;
}
/* Resolve -l<name> to a filesystem path by walking the libdirs we
* collected. Order: lib<name>.so → lib<name>.so.<N> globs → lib<name>.a.
* Skip anything that isn't a real archive or ELF (e.g. ld scripts).
*/
static const char *
resolve_lib(const char *name, char **libdirs, int n_libdirs)
{
static char buf[1024];
for (int i = 0; i < n_libdirs; i++) {
snprintf(buf, sizeof buf, "%s/lib%s.so", libdirs[i], name);
if (access(buf, 0) == 0 && is_linkable(buf)) return strdup(buf);
for (int v = 0; v <= 8; v++) {
snprintf(buf, sizeof buf, "%s/lib%s.so.%d",
libdirs[i], name, v);
if (access(buf, 0) == 0 && is_linkable(buf))
return strdup(buf);
}
snprintf(buf, sizeof buf, "%s/lib%s.a", libdirs[i], name);
if (access(buf, 0) == 0 && is_linkable(buf)) return strdup(buf);
}
return NULL;
}
int
main(int argc, char **argv)
{
const char *out = NULL;
const char **inputs = calloc(argc, sizeof *inputs);
int ninputs = 0;
char **libdirs = calloc(argc, sizeof *libdirs);
int n_libdirs = 0;
const char **lflags = calloc(argc, sizeof *lflags);
int n_lflags = 0;
u64 base = 0x400000;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
out = argv[++i];
} else if (strncmp(argv[i], "-L", 2) == 0 && argv[i][2]) {
libdirs[n_libdirs++] = strdup(argv[i] + 2);
} else if (strcmp(argv[i], "-L") == 0 && i + 1 < argc) {
libdirs[n_libdirs++] = strdup(argv[++i]);
} else if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) {
lflags[n_lflags++] = argv[i] + 2;
} else if (strcmp(argv[i], "-l") == 0 && i + 1 < argc) {
lflags[n_lflags++] = argv[++i];
} else if (argv[i][0] == '-') {
fprintf(stderr, "w6l: unknown flag %s\n", argv[i]);
return 2;
} else {
inputs[ninputs++] = argv[i];
}
}
if (out == NULL || ninputs == 0) {
fputs("usage: w6l -o exe [-L<dir>...] [-l<name>...] "
"file1.o [file2.o...]\n", stderr);
return 2;
}
/* Append -l-resolved files to the input list, after the .o/.a the
* caller passed positionally. They obey the same archive-pull
* semantics as a positional .a. */
for (int i = 0; i < n_lflags; i++) {
const char *p = resolve_lib(lflags[i], libdirs, n_libdirs);
if (p == NULL) {
fprintf(stderr, "w6l: cannot find -l%s\n", lflags[i]);
return 1;
}
inputs[ninputs++] = p;
}
Lnk l = {0};
/* Seed the symbol table with the entry point so archive pulls
* include the .o that defines it. Without this, a libwwrt.a
* containing start.o is silently skipped if no user .o
* references _start, and the entry falls back to main — which
* has no proper exit path. */
(void)l_intern(&l, "_start");
for (int i = 0; i < ninputs; i++) {
if (l_load(&l, inputs[i]) != 0) return 1;
}
if (l_resolve(&l) != 0) return 1;
if (l_relocate(&l, base + 0x1000) != 0) return 1;
Lsym *entry = l_lookup(&l, "_start");
if (entry == NULL || !entry->defined) entry = l_lookup(&l, "main");
if (entry == NULL || !entry->defined) {
fprintf(stderr, "w6l: no _start or main symbol defined\n");
return 1;
}
FILE *f = fopen(out, "wb");
if (f == NULL) {
fprintf(stderr, "w6l: cannot open %s\n", out);
return 1;
}
int rc = l_emit_elf(&l, f, base, base + 0x1000 + entry->val);
fclose(f);
if (rc == 0) {
/* chmod +x */
char cmd[1024];
snprintf(cmd, sizeof cmd, "chmod +x %s", out);
(void)system(cmd);
}
free(inputs);
return rc;
}

336
cmd/w6l/obj.c Normal file
View File

@@ -0,0 +1,336 @@
/*
* obj.c — load an ELF64 relocatable object emitted by w6a, append its
* .text bytes to the combined image, and pull its symbols and
* relocations into the global tables (with offsets adjusted to the
* combined section).
*/
#include "l.h"
#include <stdlib.h>
#include <string.h>
#define ET_REL 1
#define EM_X86_64 62
#define SHT_PROGBITS 1
#define SHT_SYMTAB 2
#define SHT_STRTAB 3
#define SHT_RELA 4
#pragma pack(push, 1)
typedef struct {
u8 e_ident[16];
u16 e_type, e_machine;
u32 e_version;
u64 e_entry, e_phoff, e_shoff;
u32 e_flags;
u16 e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx;
} Ehdr;
typedef struct {
u32 sh_name, sh_type;
u64 sh_flags, sh_addr, sh_offset, sh_size;
u32 sh_link, sh_info;
u64 sh_addralign, sh_entsize;
} Shdr;
typedef struct {
u32 st_name;
u8 st_info, st_other;
u16 st_shndx;
u64 st_value, st_size;
} Sym64;
typedef struct {
u64 r_offset;
u64 r_info;
i64 r_addend;
} Rela64;
#pragma pack(pop)
#define ELF64_R_SYM(i) ((u32)((i) >> 32))
#define ELF64_R_TYPE(i) ((u32)((i) & 0xffffffff))
#define ELF64_ST_TYPE(i) ((i) & 0xf)
#define ELF64_ST_BIND(i) ((i) >> 4)
int
l_read_all(const char *path, u8 **out, u64 *len)
{
FILE *f = fopen(path, "rb");
if (f == NULL) return -1;
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
if (n < 0) { fclose(f); return -1; }
u8 *b = malloc((size_t)n);
if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; }
fclose(f);
*out = b;
*len = (u64)n;
return 0;
}
#define read_all l_read_all
static void
emit_text(Lnk *l, const u8 *src, u64 n)
{
if (l->textlen + n > l->textcap) {
u64 nc = l->textcap ? l->textcap * 2 : 4096;
while (nc < l->textlen + n) nc *= 2;
l->text = realloc(l->text, nc);
l->textcap = nc;
}
memcpy(l->text + l->textlen, src, n);
l->textlen += n;
}
/* Internal: load a single ELF .o image already in memory. The caller
* gives us the bytes (we own them) and a path tag for diagnostics.
* If the bytes look like an archive (magic "!<arch>\n") we recurse
* over each member instead.
*/
static int load_image(Lnk *l, const char *path, u8 *buf, u64 len);
static u64
ar_field(const u8 *p, int n)
{
/* decimal field, space-padded */
u64 v = 0;
for (int i = 0; i < n; i++) {
if (p[i] >= '0' && p[i] <= '9') v = v * 10 + (p[i] - '0');
else if (p[i] == ' ') break;
else if (p[i] == 0) break;
}
return v;
}
/* Read an ELF .o image's globally-defined symbol names without
* actually appending it to the link. Returns a heap-allocated
* NULL-terminated array; caller frees the array (not the strings,
* which point into the .o image and must remain alive).
*/
static char **
elf_globals(const u8 *buf, u64 len)
{
if (len < sizeof(Ehdr)) return NULL;
Ehdr *eh = (Ehdr *)buf;
if (memcmp(eh->e_ident, "\x7f""ELF", 4) != 0) return NULL;
Shdr *sh = (Shdr *)(buf + eh->e_shoff);
int idx_text = -1, idx_symtab = -1;
const char *shstr = (const char *)(buf + sh[eh->e_shstrndx].sh_offset);
for (u16 i = 0; i < eh->e_shnum; i++) {
if (sh[i].sh_type == SHT_PROGBITS &&
strcmp(shstr + sh[i].sh_name, ".text") == 0)
idx_text = i;
else if (sh[i].sh_type == SHT_SYMTAB)
idx_symtab = i;
}
if (idx_text < 0 || idx_symtab < 0) return NULL;
int idx_strtab = sh[idx_symtab].sh_link;
Sym64 *symtab = (Sym64 *)(buf + sh[idx_symtab].sh_offset);
u64 nsyms = sh[idx_symtab].sh_size / sizeof(Sym64);
const char *str = (const char *)(buf + sh[idx_strtab].sh_offset);
char **out = calloc(nsyms + 1, sizeof *out);
int n = 0;
for (u64 i = 1; i < nsyms; i++) {
if (symtab[i].st_shndx == 0) continue;
if ((symtab[i].st_info >> 4) != 1) continue; /* STB_GLOBAL */
if ((int)symtab[i].st_shndx != idx_text) continue;
out[n++] = strdup(str + symtab[i].st_name);
}
out[n] = NULL;
return out;
}
typedef struct ArMember ArMember;
struct ArMember {
u8 *data; /* heap copy; freed if never loaded */
u64 size;
char **defs; /* NULL-terminated list of defined globals */
int loaded;
ArMember *next;
};
static int
member_defines_undef(Lnk *l, ArMember *m)
{
if (m->defs == NULL) return 0;
for (int i = 0; m->defs[i]; i++) {
Lsym *s = l_lookup(l, m->defs[i]);
if (s != NULL && !s->defined) return 1;
}
return 0;
}
static int
load_archive(Lnk *l, const char *path, u8 *buf, u64 len)
{
/* Pass 1: index members. We copy each member's bytes (cheap; few
* tens of KB per stdlib module) so the archive buffer can be
* freed once we're done indexing. */
ArMember *head = NULL, *tail = NULL;
u64 pos = 8; /* past "!<arch>\n" */
while (pos + 60 <= len) {
const u8 *hdr = buf + pos;
u64 size = ar_field(hdr + 48, 10);
u64 hdr_end = pos + 60;
if (hdr_end + size > len) break;
if (hdr[0] != '/' && hdr[0] != 0 && hdr[0] != ' ') {
ArMember *m = calloc(1, sizeof *m);
m->size = size;
m->data = malloc((size_t)size);
memcpy(m->data, buf + hdr_end, (size_t)size);
m->defs = elf_globals(m->data, size);
if (head == NULL) head = m;
else tail->next = m;
tail = m;
}
pos = hdr_end + size;
if (size & 1) pos++;
}
free(buf);
/* Pass 2: iteratively pull members that define a currently-
* undefined symbol. Each pull may introduce new undefs, so loop. */
int changed = 1;
while (changed) {
changed = 0;
for (ArMember *m = head; m; m = m->next) {
if (m->loaded) continue;
if (!member_defines_undef(l, m)) continue;
u8 *copy = malloc((size_t)m->size);
memcpy(copy, m->data, m->size);
if (load_image(l, path, copy, m->size) == 0) {
m->loaded = 1;
changed = 1;
}
}
}
/* Free unloaded members; loaded ones had their bytes consumed
* by load_image (which took the copy). */
while (head) {
ArMember *next = head->next;
free(head->data);
if (head->defs) {
for (int i = 0; head->defs[i]; i++) free(head->defs[i]);
free(head->defs);
}
free(head);
head = next;
}
return 0;
}
int
l_load(Lnk *l, const char *path)
{
u8 *buf;
u64 len;
if (read_all(path, &buf, &len) < 0) return -1;
if (len >= 8 && memcmp(buf, "!<arch>\n", 8) == 0)
return load_archive(l, path, buf, len);
/* Shared object: dispatch to dyn.c, which re-reads (small loss
* for a much cleaner separation of static vs dynamic loaders). */
if (len >= sizeof(Ehdr)) {
Ehdr *eh = (Ehdr *)buf;
if (memcmp(eh->e_ident, "\x7f""ELF", 4) == 0
&& eh->e_type == 3 /* ET_DYN */) {
free(buf);
return l_load_so(l, path);
}
}
return load_image(l, path, buf, len);
}
static int
load_image(Lnk *l, const char *path, u8 *buf, u64 len)
{
if (len < sizeof(Ehdr)) { free(buf); return -1; }
Ehdr *eh = (Ehdr *)buf;
if (memcmp(eh->e_ident, "\x7f""ELF", 4) != 0 || eh->e_ident[4] != 2
|| eh->e_machine != EM_X86_64 || eh->e_type != ET_REL) {
fprintf(stderr, "w6l: %s: not an amd64 ELF64 relocatable\n", path);
free(buf);
return -1;
}
Shdr *sh = (Shdr *)(buf + eh->e_shoff);
if (eh->e_shstrndx >= eh->e_shnum) { free(buf); return -1; }
const char *shstr = (const char *)(buf + sh[eh->e_shstrndx].sh_offset);
/* find .text, .symtab, .strtab, .rela.text */
int idx_text = -1, idx_symtab = -1, idx_strtab = -1, idx_rela = -1;
for (u16 i = 0; i < eh->e_shnum; i++) {
const char *nm = shstr + sh[i].sh_name;
if (sh[i].sh_type == SHT_PROGBITS && strcmp(nm, ".text") == 0)
idx_text = i;
else if (sh[i].sh_type == SHT_SYMTAB)
idx_symtab = i;
else if (sh[i].sh_type == SHT_RELA && strcmp(nm, ".rela.text") == 0)
idx_rela = i;
}
if (idx_text < 0 || idx_symtab < 0) {
fprintf(stderr, "w6l: %s: missing .text or .symtab\n", path);
free(buf);
return -1;
}
idx_strtab = sh[idx_symtab].sh_link;
Lobj *ob = calloc(1, sizeof *ob);
ob->path = strdup(path);
ob->buf = buf;
ob->len = len;
ob->text_off = l->textlen;
ob->text_size = sh[idx_text].sh_size;
ob->next = l->objs;
l->objs = ob;
/* append .text */
emit_text(l, buf + sh[idx_text].sh_offset, sh[idx_text].sh_size);
/* per-object: load symbols */
Sym64 *symtab = (Sym64 *)(buf + sh[idx_symtab].sh_offset);
u64 nsyms = sh[idx_symtab].sh_size / sizeof(Sym64);
const char *str = (const char *)(buf + sh[idx_strtab].sh_offset);
/* map per-object sym index → global Lsym */
Lsym **map = calloc(nsyms, sizeof *map);
for (u64 i = 1; i < nsyms; i++) {
const char *nm = str + symtab[i].st_name;
if (nm[0] == '\0') continue;
Lsym *gs = l_intern(l, nm);
if (symtab[i].st_shndx != 0 /* SHN_UNDEF */
&& symtab[i].st_shndx == idx_text) {
if (gs->defined) {
fprintf(stderr, "w6l: %s: duplicate symbol %s\n",
path, nm);
l->errs++;
} else {
gs->defined = 1;
gs->owner = ob;
gs->idx_in_owner = (int)i;
gs->val = ob->text_off + symtab[i].st_value;
}
}
map[i] = gs;
}
/* per-object: collect relocations */
if (idx_rela >= 0) {
Rela64 *rt = (Rela64 *)(buf + sh[idx_rela].sh_offset);
u64 nrel = sh[idx_rela].sh_size / sizeof(Rela64);
for (u64 i = 0; i < nrel; i++) {
Lrel *r = calloc(1, sizeof *r);
r->off = ob->text_off + rt[i].r_offset;
r->kind = (int)ELF64_R_TYPE(rt[i].r_info);
u32 sidx = ELF64_R_SYM(rt[i].r_info);
r->sym = (sidx < nsyms) ? map[sidx] : NULL;
r->addend = rt[i].r_addend;
r->next = l->rels;
l->rels = r;
}
}
free(map);
return 0;
}

94
cmd/w6l/out.c Normal file
View File

@@ -0,0 +1,94 @@
/*
* out.c — emit a static ELF64 executable.
*
* Layout (file order):
* [0..64) ELF header
* [64..120) program header (one PT_LOAD)
* [120..0x1000) zero pad
* [0x1000..) .text bytes
*
* The single PT_LOAD covers the whole file, R+X. No interpreter,
* no dynamic, no .bss yet. Entry point is the address of the
* symbol named "_start" (or whatever main supplies via -e).
*/
#include "l.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ET_EXEC 2
#define EM_X86_64 62
#define EV_CURRENT 1
#define ELFCLASS64 2
#define ELFDATA2LSB 1
#define PT_LOAD 1
#define PF_X 1
#define PF_W 2
#define PF_R 4
#pragma pack(push, 1)
typedef struct {
u8 e_ident[16];
u16 e_type, e_machine;
u32 e_version;
u64 e_entry, e_phoff, e_shoff;
u32 e_flags;
u16 e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx;
} Ehdr;
typedef struct {
u32 p_type, p_flags;
u64 p_offset, p_vaddr, p_paddr;
u64 p_filesz, p_memsz, p_align;
} Phdr;
#pragma pack(pop)
int
l_emit_elf(Lnk *l, FILE *f, u64 base, u64 entry)
{
/* Dispatch: any loaded shared object plus any dynamic ref means
* we owe the loader a real PT_INTERP/PT_DYNAMIC binary. */
if (l->sos != NULL && l->dyn_n > 0)
return l_emit_dyn_elf(l, f, base, entry);
const u64 text_off = 0x1000;
const u64 text_va = base + text_off;
const u64 filesz = text_off + l->textlen;
Ehdr eh = {0};
memcpy(eh.e_ident, "\x7f""ELF", 4);
eh.e_ident[4] = ELFCLASS64;
eh.e_ident[5] = ELFDATA2LSB;
eh.e_ident[6] = EV_CURRENT;
eh.e_type = ET_EXEC;
eh.e_machine = EM_X86_64;
eh.e_version = EV_CURRENT;
eh.e_entry = entry;
eh.e_phoff = sizeof(Ehdr);
eh.e_ehsize = sizeof(Ehdr);
eh.e_phentsize = sizeof(Phdr);
eh.e_phnum = 1;
(void)text_va;
Phdr ph = {0};
ph.p_type = PT_LOAD;
ph.p_flags = PF_R | PF_X;
ph.p_offset = 0;
ph.p_vaddr = base;
ph.p_paddr = base;
ph.p_filesz = filesz;
ph.p_memsz = filesz;
ph.p_align = 0x1000;
fwrite(&eh, 1, sizeof eh, f);
fwrite(&ph, 1, sizeof ph, f);
/* pad to text_off */
long here = ftell(f);
for (long i = here; i < (long)text_off; i++) fputc(0, f);
if (l->textlen) fwrite(l->text, 1, l->textlen, f);
return 0;
}

95
cmd/w6l/pass.c Normal file
View File

@@ -0,0 +1,95 @@
/*
* pass.c — resolution + relocation. After all objects are loaded:
*
* l_resolve : check that every symbol referenced by a relocation
* is defined somewhere. Errors get logged.
* l_relocate: with the final virtual base address known, walk the
* relocation list and patch the .text bytes in place.
*
* Supported relocation kinds: PC32 (2), PLT32 (4). Both are PC-relative
* 32-bit displacements; for static linking PLT32 collapses to PC32.
*/
#include "l.h"
#include <stdio.h>
#include <string.h>
#define R_X86_64_PC32 2
#define R_X86_64_PLT32 4
int
l_resolve(Lnk *l)
{
/* Initialise dynamic-linking fields. plt_idx and dynsym_idx
* default to -1; l_intern sets is_dyn/dyn_lib to 0/NULL via
* calloc, but we make the invariants explicit here for clarity. */
for (Lsym *s = l->syms; s; s = s->next) {
s->plt_idx = -1;
s->dynsym_idx = -1;
}
/* Promote each undefined sym that some Lso exports to "dynamic"
* and hand it a PLT slot. Order is the iteration order over the
* relocation list; that determines slot numbering and is stable
* across runs (rels are pushed onto the head as objects load). */
for (Lrel *r = l->rels; r; r = r->next) {
if (r->sym == NULL || r->sym->defined) continue;
if (r->sym->is_dyn) continue; /* already promoted */
for (Lso *so = l->sos; so; so = so->next) {
const char *ver = NULL;
if (l_so_provides_v(so, r->sym->name, &ver)) {
r->sym->is_dyn = 1;
r->sym->dyn_lib = so;
r->sym->dyn_version = ver; /* may be NULL */
r->sym->plt_idx = l->dyn_n++;
break;
}
}
}
/* What remains undefined truly is undefined. */
for (Lrel *r = l->rels; r; r = r->next) {
if (r->sym == NULL) continue;
if (!r->sym->defined && !r->sym->is_dyn) {
fprintf(stderr, "w6l: undefined reference to '%s'\n",
r->sym->name);
l->errs++;
}
}
return l->errs;
}
static void
patch_u32(u8 *p, u32 v)
{
p[0] = (u8)(v & 0xff);
p[1] = (u8)((v >> 8) & 0xff);
p[2] = (u8)((v >> 16) & 0xff);
p[3] = (u8)((v >> 24) & 0xff);
}
int
l_relocate(Lnk *l, u64 base)
{
for (Lrel *r = l->rels; r; r = r->next) {
if (r->sym == NULL) continue;
/* Dynamic syms are patched later, in l_emit_elf, once the
* PLT's virtual address is known. */
if (r->sym->is_dyn) continue;
if (!r->sym->defined) continue;
switch (r->kind) {
case R_X86_64_PC32:
case R_X86_64_PLT32: {
u64 site = base + r->off;
i64 target = (i64)(base + r->sym->val);
i64 rel = target - (i64)site + r->addend;
patch_u32(l->text + r->off, (u32)(i32)rel);
break;
}
default:
fprintf(stderr, "w6l: unsupported reloc kind %d\n",
r->kind);
l->errs++;
}
}
return l->errs;
}

27
cmd/w6l/sym.c Normal file
View File

@@ -0,0 +1,27 @@
/*
* sym.c — global symbol table for the linker. Plain singly-linked
* list; usually a few hundred entries, hashing isn't worth it yet.
*/
#include "l.h"
#include <stdlib.h>
#include <string.h>
Lsym *
l_intern(Lnk *l, const char *name)
{
for (Lsym *s = l->syms; s; s = s->next)
if (strcmp(s->name, name) == 0) return s;
Lsym *s = calloc(1, sizeof *s);
s->name = strdup(name);
s->next = l->syms;
l->syms = s;
return s;
}
Lsym *
l_lookup(Lnk *l, const char *name)
{
for (Lsym *s = l->syms; s; s = s->next)
if (strcmp(s->name, name) == 0) return s;
return NULL;
}