diff --git a/Makefile b/Makefile index a7c12c22..55fe341b 100644 --- a/Makefile +++ b/Makefile @@ -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_ 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 $@ $< diff --git a/cmd/6l/dyn.c b/cmd/6l/dyn.c new file mode 100644 index 00000000..39335c30 --- /dev/null +++ b/cmd/6l/dyn.c @@ -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 +#include +#include + +#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; +} diff --git a/cmd/6l/dynout.c b/cmd/6l/dynout.c new file mode 100644 index 00000000..c4538645 --- /dev/null +++ b/cmd/6l/dynout.c @@ -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 +#include +#include + +/* 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 . 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; +} diff --git a/cmd/6l/l.h b/cmd/6l/l.h index a3c86f83..c992efda 100644 --- a/cmd/6l/l.h +++ b/cmd/6l/l.h @@ -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 diff --git a/cmd/6l/main.c b/cmd/6l/main.c index b03b3075..f81c9c45 100644 --- a/cmd/6l/main.c +++ b/cmd/6l/main.c @@ -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...] [-l...] 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 #include #include +#include + +/* `path` is acceptable iff it's either an archive ("!\n") or + * an ELF file ("\x7fELF"). Distros often ship lib.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, "!\n", 8) == 0) return 1; + if (n >= 4 && memcmp(magic, "\x7f""ELF", 4) == 0) return 1; + return 0; +} + +/* Resolve -l to a filesystem path by walking the libdirs we + * collected. Order: lib.so → lib.so. globs → lib.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...] [-l...] " + "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 diff --git a/cmd/6l/obj.c b/cmd/6l/obj.c index 3ecfadae..6e111e75 100644 --- a/cmd/6l/obj.c +++ b/cmd/6l/obj.c @@ -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, "!\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); } diff --git a/cmd/6l/out.c b/cmd/6l/out.c index 0cadcf25..d0db8bd1 100644 --- a/cmd/6l/out.c +++ b/cmd/6l/out.c @@ -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; diff --git a/cmd/6l/pass.c b/cmd/6l/pass.c index c4b35cec..75620eaf 100644 --- a/cmd/6l/pass.c +++ b/cmd/6l/pass.c @@ -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: { diff --git a/cmd/ww/main.c b/cmd/ww/main.c index c31c05e2..3140b8e8 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -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 goes before -l 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 entries, space-separated */ + char libdirs[2048] = {0}; /* -L 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; diff --git a/test/wwc/810_dyn.c b/test/wwc/810_dyn.c new file mode 100644 index 00000000..abfe10e9 --- /dev/null +++ b/test/wwc/810_dyn.c @@ -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 +#include +#include +#include +#include +#include + +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; +}