MODULE 6

Dynamic Linking

Dynamic linking is the mechanism that lets multiple programs share the same library code at runtime. Instead of embedding a copy of printf in every binary, the linker leaves a placeholder that gets resolved when the program runs.

Why Shared Libraries?

Code Sharing

One copy of libc in memory serves every running program. Hundreds of processes can share the same physical pages of library code.

Smaller Binaries

Executables only contain their own code. A dynamically-linked hello world is often 10-100x smaller than its statically-linked counterpart.

Easy Updates

Fixing a bug in a shared library instantly benefits every program that uses it, without recompilation.

PLT and GOT — The Indirection Mechanism

Two data structures work together to make dynamic linking possible:

PLT (Procedure Linkage Table)

A table of small code stubs, one per external function. When your code calls printf, it actually calls printf@plt, which is a trampoline that reads the GOT to find the real address.

GOT (Global Offset Table)

A writable table of function addresses. Initially, each entry points back into the PLT. After resolution, it holds the real address of the function in the shared library.

Lazy Binding in Action

PLT/GOT Lazy Binding

Step 1 / 5
Program calls printf@plt from application code
Application Code
0x401030mov$0x1, %edi
0x401035lea0x2fcc(%rip), %rsi # "Hello"
0x40103ccallprintf@plt
0x401041xor%eax, %eax
0x401043callprintf@plt
PLT Stubprintf@plt
jmp*printf@GOT(%rip)
push$0x0 # reloc index
jmpPLT[0] # resolver
codePLT(call)
GOT Entryprintf@got
Value0x401036 (PLT+6)
Unresolved - points to PLT stub
Dynamic Resolver (ld-linux.so)
Looking upprintf
In librarylibc.so.6

The 5-Step Resolution Process

1
call printf@plt

Your code calls the PLT stub instead of the real function.

2
PLT reads GOT (points back to PLT initially)

The PLT stub does an indirect jump through the GOT. On first call, the GOT entry points back to the next PLT instruction.

3
Push relocation index, jump to resolver

The PLT pushes a relocation index and jumps to PLT[0], which calls the dynamic linker's resolver.

4
Resolver patches GOT with real address

The dynamic linker looks up printf in libc, finds its address, and overwrites the GOT entry.

5
Second call goes directly through GOT

Now the GOT entry holds the real address. The PLT stub jumps straight to libc's printf, bypassing the resolver.

💡Lazy Binding
Lazy binding is a performance optimization. Only resolve symbols that are actually called. A program might link against hundreds of functions but only use a handful in any given run. Lazy binding means startup isn't penalized by resolving every symbol upfront.

.dynamic — Library Dependencies

The .dynamic section is an array of tag-value pairs that tell the dynamic linker everything it needs. Key entries include:

TagPurpose
DT_NEEDEDShared library dependency (e.g., libc.so.6)
DT_STRTABAddress of the dynamic string table
DT_SYMTABAddress of the dynamic symbol table
DT_PLTGOTAddress of the GOT
DT_JMPRELAddress of PLT relocation entries

.rela.plt and .rela.dyn — Relocation Entries

Relocation entries tell the dynamic linker which GOT slots to patch and how. .rela.plt handles function calls through the PLT (R_X86_64_JUMP_SLOT), while .rela.dyn handles data references like global variable addresses (R_X86_64_GLOB_DAT).

Challenge

After lazy resolution, what does the GOT entry contain?