Syscall / ELF · Linux userspace / kernel ABI

The dynamic linker, PLT/GOT, and relocations

Examines how DT_NEEDED lookup, symbol resolution, GOT updates, lazy binding, and symbol interposition change startup time and call addresses.

Series
05 / 38
Build
cc -std=c17 -Wall -Wextra -O2 -Wl,-z,relro,-z,now dynlink.c -ldl -o dynlink
Run
./dynlink
Kernel
Linux 6.18.37 LTS

Is the address of printf already fixed at link time?

When a dynamic executable is linked, the actual runtime addresses of external symbols are not known. The linker leaves relocation records and the GOT/PLT behind; after ld-linux maps the shared objects, it fills in addresses adjusted by their load biases.

With lazy binding, the first call through a PLT entry enters the resolver and updates the GOT slot. BIND_NOW or full RELRO completes relocations at startup and shortens the period during which the GOT is writable.

Structure diagram

Figure 1. Connections among PLT/GOT and actual shared-object symbols

Executable

  • call printf@PLT
  • .rela.plt: printf
  • DT_NEEDED: libc.so.6

GOT / link_map

  • printf GOT slot
  • load bias
  • symbol version

ld-linux

  • lookup scope
  • relocation write
  • mprotect RELRO

libc.so.6

  • printf symbol
  • resolved runtime VA
  • DSO PT_LOAD

The PLT is a call waypoint, while GOT slots hold runtime addresses. With full RELRO, the GOT enters a read-only region after relocation.

Call path

Figure 2. From userspace code to observable results
ELF dynamic DT_NEEDED list
ld.so search RUNPATH/cache/default
mmap DSO establish load bias
relocation record GOT/symbol addresses
call PLT or direct branch

Symbol lookup and relocations are mostly userspace code in the dynamic linker. The kernel supplies file mappings and protection changes, but does not interpret symbol semantics.

Figure 3. Major points along the kernel-internal path
load_elf_binary load interpreter
userspace ld.so dependency graph
mmap shared object segment
mprotect RELRO read-only
main resolved image

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
fs/binfmt_elf.c load_elf_interp() Load the PT_INTERP file as a separate ELF image
mm/mmap.c do_mmap(), vm_mmap_pgoff() shared object PT_LOAD segment mapping
mm/mprotect.c do_mprotect_pkey() Change RELRO page protections after relocation

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 -Wl,-z,relro,-z,now dynlink.c -ldl -o dynlink
01#define _GNU_SOURCE
02#include <dlfcn.h>
03#include <stdio.h>
04#include <stdlib.h>
05
06typedef double (*cos_fn)(double);
07
08int main(void)
09{
10    void *handle = dlopen("libm.so.6", RTLD_NOW | RTLD_LOCAL);
11    if (handle == NULL) {
12        fprintf(stderr, "dlopen: %s\n", dlerror());
13        return 1;
14    }
15
16    dlerror();
17    cos_fn fn = (cos_fn)dlsym(handle, "cos");
18    const char *error = dlerror();
19    if (error != NULL) {
20        fprintf(stderr, "dlsym: %s\n", error);
21        dlclose(handle);
22        return 1;
23    }
24
25    printf("cos(0.0) = %.1f\n", fn(0.0));
26    return dlclose(handle) != 0;
27}

Code notes

Source line 1#define _GNU_SOURCE

Exposes the dlfcn interface and related extensions from the header. The feature-test macro must be defined before every header.

Source line 10RTLD_NOW | RTLD_LOCAL

Completes undefined-symbol relocations at dlopen time and does not place symbols from this handle in the global lookup scope of objects loaded later.

Source line 16dlerror();

Clears any stale error state before dlsym. A NULL return from dlsym is not necessarily a failure because the symbol value itself may be NULL; use dlerror to decide.

Source line 17cos_fn fn =

In a POSIX environment, a dlsym return value can be used as a function pointer. ISO C alone does not generally guarantee conversion between object pointers and function pointers.

Source line 21dlclose(handle)

Drops the reference count. Do not assume the mapping disappears immediately: another dependency or a NODELETE policy may retain it.

Detailed behavior

01

Search order changes the deployed result

DT_RPATH and DT_RUNPATH have different inheritance rules, followed by LD_LIBRARY_PATH, ld.so.cache, and the default directories. Secure-execution mode for setuid/setgid programs ignores several environment variables.

Record the combination of executable and actually loaded DSOs using readelf -d, ld.so --list, and /proc/PID/maps together.

02

Interposition constrains optimization

Because a default-visibility symbol can be interposed by another object, the compiler and linker cannot always assume a local direct call. -Bsymbolic, protected/hidden visibility, and -fno-semantic-interposition change both semantics and the available optimization scope.

LD_PRELOAD hooks work for the same reason: global lookup scope and relocation order.

03

RELRO closes the write window

Leaving a relocation target in the GOT writable forever makes it easier for an arbitrary-write vulnerability to redirect control flow. Full RELRO with immediate binding moves work to startup but makes the pages read-only after relocation.

Do not rely on checksec output alone; confirm both GNU_RELRO in readelf -l and the BIND_NOW dynamic tag.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
link_mapCreated when ld.so loads an object and removed when the object is unloadedload bias, dynamic section, dependency
GOT slotReceives an address during relocation and may become read-only after RELROSymbol version and resolved address
dlopen handleRepresents a dlopen reference and is decremented by dlcloseRTLD_LOCAL/GLOBAL, NODELETE

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
shared object not foundRUNPATH/cache/architecture mismatchLD_DEBUG=libs, readelf -d
undefined symbolVersion or lookup-scope mismatchreadelf -Ws, objdump -T
A crash after dlcloseA DSO function pointer, TLS value, or object remains in useCheck the handle lifetime and reference owner

Verify it yourself

  1. Enable LD_DEBUG=libs and LD_DEBUG=bindings separately to distinguish library lookup from the moment symbols are bound.
  2. Compare startup syscalls and first-call latency between builds using -Wl,-z,lazy and -Wl,-z,now.
  3. Create a puts wrapper with LD_PRELOAD and observe interposition under combinations of default visibility and -Bsymbolic.
Run./dynlink
TraceLD_DEBUG=libs,reloc ./dynlink 2>&1 | less

Primary sources