2 — A libc, and what it costs
Requirements: chapter 1. Work in /lo/out, with $TGT exported.
What a loader is
A dynamic program doesn’t start at its own code. Its ELF file names an interpreter, and the kernel maps that into memory during execve and runs it first. The interpreter loads libraries and connects them, and only then calls your main().
In glibc, the loader (ld-linux) and the library (libc.so.6) are separate files. In musl they’re one file. ld-musl-x86_64.so.1 is a link to libc.so (musl design).
This series’ decision: static by default for everything in /bin and /sbin. With one multicall binary (chapter 3), one copy of libc serves the whole userland. There’s no loader at startup, no LD_PRELOAD surface, and no “which libc did this pick up?” question.
How musl differs from glibc
These differences cause real “works on my distro” bugs (functional differences):
| Area | musl | glibc |
|---|---|---|
| DNS | Queries nameservers in parallel, and no search fallback for names with at least ndots dots | Tries them in order |
| Thread stacks | 128 KB by default | Typically 2 to 10 MB |
| Lazy binding | No | Yes |
dlclose | Does nothing | Unloads |
| Locale | Defaults to C.UTF-8 | Depends on config |
| Static linking | Fully supported | NSS still needs shared libraries at runtime (FAQ) |
Hands-on
Step 1: compare the two files
ls -l hello-static hello-dynamic
$TGT-readelf -l hello-static | grep -c INTERP
$TGT-readelf -l hello-dynamic | grep interpreter
$TGT-readelf -d hello-dynamic | grep NEEDED
Expected: the static count is 0, the dynamic file names its interpreter, and NEEDED lists libc.so.
Step 2: prove the loader is libc
If the loader is run under a name ending in ldd, it acts as ldd (dynlink.c):
ls -l /lo/rootfs/lib/ld-musl-x86_64.so.1
ln -sf /lo/rootfs/lib/ld-musl-x86_64.so.1 /lo/out/ldd-musl
/lo/out/ldd-musl ./hello-dynamic
Expected: the link points to libc.so, and the output lists the loader’s own path.
Step 3: trace both
strace ./hello-static 2>&1 | tee static.trace | wc -l
strace /lo/rootfs/lib/ld-musl-x86_64.so.1 ./hello-dynamic 2>&1 | tee dynamic.trace | wc -l
Look for:
writev, notwrite. musl’s stdio useswritev(__stdio_write.c).- An
ioctlfirst, when output goes to a terminal, as stdio decides on line buffering. set_tid_addressduring startup.
Running through the loader explicitly makes it open hello-dynamic itself. That’s a side effect of this method. In normal use, the kernel maps the interpreter, so you won’t see an openat for it. The true trace appears in chapter 4, inside the finished system.
Step 4: hijack a function
-
Create a program and a hook. It must call
write()directly, becauseputsdoesn’t go through that function on musl:cat > w.c <<'E' #include <unistd.h> int main(void){ write(1,"real\n",5); return 0; } E cat > evil.c <<'E' #include <unistd.h> #include <sys/syscall.h> ssize_t write(int f,const void*b,size_t n){ return syscall(SYS_write,f,"hook\n",5); } E -
Build and run both versions:
$TGT-gcc w.c -o w-dyn && $TGT-gcc -static w.c -o w-static $TGT-gcc -shared -fPIC evil.c -o evil.so LD_PRELOAD=$PWD/evil.so /lo/rootfs/lib/ld-musl-x86_64.so.1 ./w-dyn LD_PRELOAD=$PWD/evil.so ./w-static -
Expected: only the dynamic one prints
hook. This is why an init should be static.
Step 5: measure the cost
perf stat -e page-faults,instructions ./hello-static
Repeat through the loader. Write down your numbers, and form your own hypothesis about any difference.
If it fails
hello-dynamic: not found: you skipped the loader path.Error loading shared library: a library is missing from musl’s path file.evil.sois ignored or fails: build it with$TGT-gcc, not the host compiler.
Principle: indirection is never free; it is sometimes worth it
Dynamic linking buys shared memory pages across processes, in-place security updates of libc, and plugins. It costs startup work, a loader to trust, and a question about which library actually ran. Neither answer is right in general; the trace tells you the price, and your system’s needs tell you whether to pay it. Note that plugins are a weaker benefit on musl, where dlclose does nothing and there’s no NSS.