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:
11
Makefile
11
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_<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
262
cmd/6l/dyn.c
Normal 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
644
cmd/6l/dynout.c
Normal 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;
|
||||
}
|
||||
30
cmd/6l/l.h
30
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
|
||||
|
||||
@@ -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
|
||||
|
||||
16
cmd/6l/obj.c
16
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, "!<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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
152
test/wwc/810_dyn.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user