Files
ww/cmd/6l/dyn.c
Hojun-Cho ecf0a84127 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).
2026-05-11 09:42:27 +09:00

263 lines
7.1 KiB
C

/*
* 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;
}