6l: dynamic linking with symbol versioning

Teach the linker to consume ET_DYN shared objects and emit a
dynamically-linked ELF executable. Snake et al. can now link
against libncurses + libc through the system dynamic loader.

Pipeline additions:

- dyn.c: read ET_DYN, parse .dynsym + DT_SONAME, walk
  .gnu.version_d / .gnu.version to learn each export's default
  version (skip hidden entries).
- pass.c: when an undefined sym is provided by some Lso,
  promote it to dynamic, assign a PLT slot, record the
  matched version on the Lsym.
- dynout.c: emit PT_INTERP + PT_DYNAMIC, .dynsym/.dynstr/.hash,
  .plt + .got.plt + .rela.plt, .gnu.version + .gnu.version_r,
  and the full DT_* set with DT_BIND_NOW. Patch PC32/PLT32
  references against dyn syms to point at their PLT stubs.
- main.c: -L<dir> and -l<name> flag parsing; resolve <name>
  via .so / .so.<N> / .a in libdir order, skipping GNU ld
  linker scripts (libc.so on most distros).
- ww driver: collect -l/-L (joined and split forms) and pass
  through to 6l.

Design choices:

- DT_BIND_NOW so the loader resolves all PLT slots at startup;
  no PLT0 lazy resolver stub.
- SysV .hash, not .gnu.hash. One bucket; loader scans the
  chain. Slow at scale, fine for snake-class binaries.
- Non-PIE at fixed 0x400000.
- No section headers — loader uses program headers, but
  readelf -V/-S won't display anything.

Symbol versioning is the only correctness item beyond the
basic PLT/GOT machinery: glibc symbols default to versions
later than GLIBC_2.2.5 (e.g. clock_gettime → GLIBC_2.17 for
the vDSO impl), and the loader rejects unversioned references
to those without a matching Vernaux entry.

test/wwc/810_dyn covers four cases: bare libc dyn call,
multi-PLT, clock_gettime versioning, and fn-pointer to FFI
binding (which exercises the codegen fixes from the parent
commit alongside the new linker path).
This commit is contained in:
2026-05-11 09:42:27 +09:00
parent 635818eb13
commit ecf0a84127
10 changed files with 1250 additions and 30 deletions

View File

@@ -34,7 +34,8 @@ C6_OBJ = $(C6_SRC:cmd/6c/%.c=$(OBJ)/6c/%.o)
A6_SRC = cmd/6a/main.c cmd/6a/lex.c cmd/6a/parse.c cmd/6a/asm.c cmd/6a/obj.c
A6_OBJ = $(A6_SRC:cmd/6a/%.c=$(OBJ)/6a/%.o)
L6_SRC = cmd/6l/main.c cmd/6l/obj.c cmd/6l/sym.c cmd/6l/pass.c cmd/6l/out.c
L6_SRC = cmd/6l/main.c cmd/6l/obj.c cmd/6l/sym.c cmd/6l/pass.c cmd/6l/out.c \
cmd/6l/dyn.c cmd/6l/dynout.c
L6_OBJ = $(L6_SRC:cmd/6l/%.c=$(OBJ)/6l/%.o)
RT_S = rt/start.s rt/syscall.s rt/alloc.s rt/streq.s rt/abort.s
@@ -170,8 +171,8 @@ $(BIN) $(LIB) $(OBJ)/wwc $(OBJ)/ww $(OBJ)/wwdump $(OBJ)/6c $(OBJ)/6a $(OBJ)/6l $
# Each phase adds a $(BIN)/test_<name> target; the runner walks them.
TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_6c $(BIN)/test_6a $(BIN)/test_6l $(BIN)/test_arch \
$(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_stdlib $(BIN)/test_selfhost \
$(BIN)/test_6a_ww $(BIN)/test_6l_ww $(BIN)/test_ww_ww
$(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \
$(BIN)/test_selfhost $(BIN)/test_6a_ww $(BIN)/test_6l_ww $(BIN)/test_ww_ww
$(BIN)/test_smoke: test/wwc/000_smoke.c $(LIB)/libwwc.a | $(BIN)
$(CC) $(CFLAGS) $(INCS) -o $@ $< -L$(LIB) -lwwc
@@ -205,6 +206,10 @@ $(BIN)/test_e2e: test/wwc/700_e2e.c $(BIN)/ww $(BIN)/6c $(BIN)/6a $(BIN)/6l \
$(BIN)/test_ffi: test/wwc/800_ffi.c $(BIN)/6c | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_dyn: test/wwc/810_dyn.c $(BIN)/ww $(BIN)/6c $(BIN)/6a $(BIN)/6l \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_stdlib: test/wwc/900_stdlib.c $(BIN)/6c | $(BIN)
$(CC) $(CFLAGS) -o $@ $<

262
cmd/6l/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, "6l: %s: cannot read\n", path);
return -1;
}
if (len < sizeof(Ehdr)) {
fprintf(stderr, "6l: %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, "6l: %s: not an amd64 ET_DYN\n", path);
free(buf);
return -1;
}
if (eh->e_shoff == 0 || eh->e_shnum == 0) {
fprintf(stderr, "6l: %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, "6l: %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/6l/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, "6l: 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, "6l: 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, "6l: 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, "6l: 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;
}

View File

@@ -23,6 +23,7 @@ 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 {
@@ -31,6 +32,14 @@ struct Lsym {
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;
};
@@ -51,17 +60,35 @@ struct Lobj {
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);
@@ -70,5 +97,8 @@ 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

View File

@@ -1,8 +1,10 @@
/*
* 6l — amd64 static linker. Reads relocatable ELF .o files, resolves,
* relocates, writes a static ELF executable.
* 6l — amd64 linker. Reads relocatable ELF .o files (from 6a) 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.
*
* 6l -o out file1.o file2.o ...
* 6l -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".
@@ -11,6 +13,48 @@
#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)
@@ -18,11 +62,23 @@ 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, "6l: unknown flag %s\n", argv[i]);
return 2;
@@ -31,10 +87,23 @@ main(int argc, char **argv)
}
}
if (out == NULL || ninputs == 0) {
fputs("usage: 6l -o exe file1.o [file2.o...]\n", stderr);
fputs("usage: 6l -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, "6l: 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

View File

@@ -52,8 +52,8 @@ typedef struct {
#define ELF64_ST_TYPE(i) ((i) & 0xf)
#define ELF64_ST_BIND(i) ((i) >> 4)
static int
read_all(const char *path, u8 **out, u64 *len)
int
l_read_all(const char *path, u8 **out, u64 *len)
{
FILE *f = fopen(path, "rb");
if (f == NULL) return -1;
@@ -69,6 +69,8 @@ read_all(const char *path, u8 **out, u64 *len)
return 0;
}
#define read_all l_read_all
static void
emit_text(Lnk *l, const u8 *src, u64 n)
{
@@ -229,6 +231,16 @@ l_load(Lnk *l, const char *path)
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);
}

View File

@@ -47,6 +47,11 @@ typedef struct {
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;

View File

@@ -19,9 +19,37 @@
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) {
if (!r->sym->defined && !r->sym->is_dyn) {
fprintf(stderr, "6l: undefined reference to '%s'\n",
r->sym->name);
l->errs++;
@@ -43,7 +71,11 @@ int
l_relocate(Lnk *l, u64 base)
{
for (Lrel *r = l->rels; r; r = r->next) {
if (r->sym == NULL || !r->sym->defined) continue;
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: {

View File

@@ -156,7 +156,8 @@ expand(FILE *out, const char *path, struct ImportSet *visited,
}
static int
build_one(const char *src, const char *out, const char *extra_includes)
build_one(const char *src, const char *out, const char *extra_includes,
const char *extra_libs, const char *extra_libdirs)
{
const char *c6 = toolpath("WW_6C", "6c");
const char *a6 = toolpath("WW_6A", "6a");
@@ -237,7 +238,13 @@ build_one(const char *src, const char *out, const char *extra_includes)
snprintf(a2, sizeof a2, "%s/../obj/rt/syscall.o", self_dir);
snprintf(rtargs, sizeof rtargs, "%s %s", a1, a2);
}
snprintf(cmd, sizeof cmd, "%s -o %s %s %s", l6, out, obj, rtargs);
/* -L<dir> goes before -l<name> so 6l can resolve the latter. */
const char *libargs = (extra_libs && extra_libs[0]) ? extra_libs : "";
const char *libdirset = (extra_libdirs && extra_libdirs[0]) ? extra_libdirs : "";
snprintf(cmd, sizeof cmd, "%s -o %s %s %s%s%s%s%s",
l6, out, obj, rtargs,
libdirset[0] ? " " : "", libdirset,
libargs[0] ? " " : "", libargs);
if (run(cmd) != 0) {
fprintf(stderr, "ww: 6l failed\n");
return 1;
@@ -256,21 +263,26 @@ static int
do_build(int argc, char **argv)
{
const char *src = NULL;
char libs[2048] = {0};
char incs[2048] = {0};
char libs[2048] = {0}; /* -l<name> entries, space-separated */
char libdirs[2048] = {0}; /* -L<dir> entries, space-separated */
char incs[2048] = {0};
for (int i = 0; i < argc; i++) {
if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) {
char libpath[512];
const char *libdir = getenv("WW_LIB");
if (libdir == NULL) {
static char def[1024];
snprintf(def, sizeof def, "%s/../lib", self_dir);
libdir = def;
}
snprintf(libpath, sizeof libpath, "%s/lib%s.a",
libdir, argv[i] + 2);
size_t n = strlen(libs);
snprintf(libs + n, sizeof libs - n, " %s", libpath);
snprintf(libs + n, sizeof libs - n,
"%s%s", n ? " " : "", argv[i]);
} else if (strcmp(argv[i], "-l") == 0 && i + 1 < argc) {
size_t n = strlen(libs);
snprintf(libs + n, sizeof libs - n,
"%s-l%s", n ? " " : "", argv[++i]);
} else if (strcmp(argv[i], "-L") == 0 && i + 1 < argc) {
size_t n = strlen(libdirs);
snprintf(libdirs + n, sizeof libdirs - n,
"%s-L%s", n ? " " : "", argv[++i]);
} else if (strncmp(argv[i], "-L", 2) == 0 && argv[i][2]) {
size_t n = strlen(libdirs);
snprintf(libdirs + n, sizeof libdirs - n,
"%s%s", n ? " " : "", argv[i]);
} else if (strcmp(argv[i], "-I") == 0 && i + 1 < argc) {
size_t n = strlen(incs);
snprintf(incs + n, sizeof incs - n,
@@ -290,10 +302,7 @@ do_build(int argc, char **argv)
snprintf(out, sizeof out, "%s", base);
char *dot = strrchr(out, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
(void)libs; /* libs string is gathered; build_one currently
* always links libwwrt.a; -l support pending more
* glue between driver and 6l invocation. */
return build_one(src, out, incs);
return build_one(src, out, incs, libs, libdirs);
}
static int
@@ -302,7 +311,7 @@ do_run(int argc, char **argv)
if (argc < 1) { fputs("ww run: missing source\n", stderr); return 2; }
char tmp[1024];
snprintf(tmp, sizeof tmp, "/tmp/ww_run_%d", getpid());
if (build_one(argv[0], tmp, "") != 0) return 1;
if (build_one(argv[0], tmp, "", "", "") != 0) return 1;
int rc = run(tmp);
unlink(tmp);
return rc;

152
test/wwc/810_dyn.c Normal file
View File

@@ -0,0 +1,152 @@
/*
* 810_dyn — end-to-end test of 6l's dynamic linker.
*
* Drives `ww build -L /usr/lib -l c` over a small ww program that
* binds libc symbols via @symbol, then runs the produced binary and
* checks the exit status. Verifies:
*
* - the .so loader (dyn.c) reads ET_DYN and extracts exports
* - PLT/GOT generation and the JUMP_SLOT relocation
* - PT_INTERP/PT_DYNAMIC emission
* - symbol versioning (.gnu.version + .gnu.version_r) — clock_gettime
* has both GLIBC_2.2.5 (compat) and GLIBC_2.17 (default) on glibc;
* the loader requires the right .gnu.version_r entry to bind
* the default vDSO-aware impl.
*
* Skipped (passing trivially) on systems without /usr/lib/libc.so.6.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return 1;
}
struct row {
const char *src;
int want_exit;
};
static const struct row rows[] = {
/* 1. _exit via dynamically-linked libc. Exit code is the plumbing
* of choice — proves the PLT entry calls into libc.so.6. */
{ "@symbol(\"_exit\") fn libc_exit(c: i32) void;\n"
"export fn main() i32 = { libc_exit(42); return 0; };", 42 },
/* 2. Multiple dyn syms in one binary: write + _exit. */
{ "@symbol(\"write\") fn libc_write(fd: i32, b: *u8, n: u64) i64;\n"
"@symbol(\"_exit\") fn libc_exit(c: i32) void;\n"
"export fn main() i32 = {\n"
" libc_write(1, \"x\".ptr, 1u64);\n"
" libc_exit(7);\n"
" return 0;\n"
"};", 7 },
/* 3. Symbol versioning: clock_gettime. Without DT_VERSYM/VERNEED
* pointing at a Vernaux entry for GLIBC_2.17, the loader either
* picks the wrong impl or fails outright on modern glibc. */
{ "@symbol(\"clock_gettime\") fn clock_gettime(c: i32, ts: *u8) i32;\n"
"@symbol(\"_exit\") fn libc_exit(c: i32) void;\n"
"export fn main() i32 = {\n"
" let ts: [16]u8;\n"
" let r: i32 = clock_gettime(1, ts.ptr);\n" /* CLOCK_MONOTONIC */
" libc_exit(r);\n"
" return 0;\n"
"};", 0 },
/* 4. Take the address of an FFI binding and call it indirectly.
* Codegen must apply @symbol resolution at the LEAQ site (so the
* stub address is `write`, not the ww-side ident `libc_write`),
* and the local-variable call must be CALL *AX, not CALL fp(SB). */
{ "@symbol(\"write\") fn libc_write(fd: i32, b: *u8, n: u64) i64;\n"
"@symbol(\"_exit\") fn libc_exit(c: i32) void;\n"
"export fn main() i32 = {\n"
" let fp: fn(fd: i32, b: *u8, n: u64) i64 = libc_write;\n"
" fp(1, \"X\".ptr, 1u64);\n"
" libc_exit(5);\n"
" return 0;\n"
"};", 5 },
{ NULL, 0 }
};
int
main(void)
{
if (access("/usr/lib/libc.so.6", 0) != 0
&& access("/lib/x86_64-linux-gnu/libc.so.6", 0) != 0
&& access("/lib64/libc.so.6", 0) != 0) {
puts("dyn: no libc.so.6 on this system — skipping");
return 0;
}
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
/* Probe each common libc dir to find the right -L. */
const char *libdir = NULL;
if (access("/usr/lib/libc.so.6", 0) == 0) libdir = "/usr/lib";
else if (access("/lib/x86_64-linux-gnu/libc.so.6", 0) == 0) libdir = "/lib/x86_64-linux-gnu";
else if (access("/lib64/libc.so.6", 0) == 0) libdir = "/lib64";
int n = 0, fail = 0;
for (int i = 0; rows[i].src; i++, n++) {
char src[80], exe[80], tmpdir[80];
snprintf(src, sizeof src, "/tmp/wwdyn_%d_%d.ww", getpid(), i);
snprintf(exe, sizeof exe, "/tmp/wwdyn_%d_%d", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwdyn_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
FILE *f = fopen(src, "wb");
fputs(rows[i].src, f);
fclose(f);
char cmd[1024];
snprintf(cmd, sizeof cmd,
"cd %s && %s/ww build %s -L %s -l c",
tmpdir, bin, src, libdir);
if (runwait(cmd) != 0) {
fprintf(stderr, "dyn row %d: build failed\n", i);
fail++;
unlink(src); rmdir(tmpdir);
continue;
}
char outbin[160];
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
if (got != rows[i].want_exit) {
fprintf(stderr, "dyn row %d: exit %d, want %d\n",
i, got, rows[i].want_exit);
fail++;
}
unlink(src); unlink(outbin); rmdir(tmpdir);
}
if (fail) {
fprintf(stderr, "%d/%d dyn tests failed\n", fail, n);
return 1;
}
printf("dyn: %d/%d ok\n", n, n);
return 0;
}