← All series

The World of Linkers

Follow symbols and addresses to understand how programs are linked, loaded and run.

33 articles · In reading order

Theory

[The World of Linkers—Theory 00] From a Function Call to a Running Program

Follow a call across two source files to discover what the compiler, linker, and loader each know. Inspect a real Linux executable, diagnose failures at four stages, and establish the native environment used throughout the series.

[The World of Linkers—Theory 01] The Long Road from Names to Addresses

A linker began as a way to move reusable code. Follow the problems that led to object files, shared libraries, unwind tables, security metadata, and modern parallel linkers—then link a small program by hand.

[The World of Linkers—Theory 02] An Object File Is a Program with Unfinished Business

Follow C through assembly into ELF, decode the actual bytes of sections and symbols, and identify the promises a relocation record leaves for the linker. All examples use a native x86-64 Linux toolchain.

[The World of Linkers—Theory 03] One Name, Several Definitions: Who Wins?

Start with a runnable two-file program, then change only the link inputs to expose missing definitions, duplicate definitions, weak symbols, common storage, archive extraction, and the limits of ordinary type checking.

[The World of Linkers—Theory 04] Relocation: Four Bytes Between Caller and Callee

Follow a runnable Linux program from an unresolved call to four final instruction bytes. Distinguish section offsets, file offsets, P, and next RIP before deriving S+A-P; then explore negative displacements, absolute pointers, addends, overflow, and instruction relaxation.

[The World of Linkers—Theory 05] A Place for Every Section—and an Exit for Dead Code

Build the bridge from input sections to loadable segments. Compare GNU ld and LLD layouts, calculate file offsets and virtual addresses, trace section garbage collection, and test what happens when a linker script gets page permissions wrong.

[The World of Linkers—Theory 06] Before main Gets a Turn

Follow an ELF file through kernel mappings, zero-filled memory, demand paging, the initial stack, and C runtime startup. Then watch static PIE relocate itself and malformed images reveal the loader's validation boundaries.

[The World of Linkers—Theory 07] Leave the Last Address to Runtime

Discover the runtime contract behind shared libraries: PIC, GOT and PLT entries, relocation classes, lazy binding, RELRO, interposition, symbol versions, and loader search paths. Native experiments show which decisions survive until a program actually starts.

[The World of Linkers—Theory 08] Finding the Way Back Up the Stack

A return address is only the beginning. Decode real CIEs and FDEs, follow a C++ exception through its two phases, and see how the linker makes unwind information survive garbage collection and become searchable at runtime.

[The World of Linkers—Theory 09] One Variable, a Different Address in Every Thread

Follow a thread-local variable from its ELF initialization template to the current thread's memory. Decode four access models, watch the linker replace general code with fixed offsets, and reproduce the static TLS limit encountered by dynamically loaded libraries.

[The World of Linkers—Theory 10] When a Program Must Arrange Its Own Memory

Linker scripts organize input sections and define the address contract for an image. Distinguish file offsets, VMA, and LMA; explain startup mappings, data copying, and BSS clearing; then examine kernel layouts, embedded data, section retention, alignment, and script assertions.

[The World of Linkers—Theory 11] Teaching Machine Code to Remember Its Source

Follow a deleted function through DWARF tombstones, debug relocations, string merging, compression, separate debug files, build IDs, and split DWARF—and test whether the debugger still tells the truth.

[The World of Linkers—Theory 12] What Changes When the Optimizer Can See Across Files?

Watch a cross-file call disappear, inspect the contract between symbol resolution and LTO, measure ThinLTO caching, then explore identical code folding and function layout without confusing smaller output with correct or faster output.

[The World of Linkers—Theory 13] The Library Changed. What Happens to the Old Program?

Break a library without changing its function names, then examine SONAMEs, symbol versions, old glibc baselines, and ABI analysis tools—including the compatibility failures those tools cannot decide for you.

[The World of Linkers—Theory 14] What C++ and Rust Ask of a Linker

Decode compiler-generated names, separate weak binding from COMDAT selection, diagnose missing vtables and initialization order, then follow Rust metadata, runtime libraries, panic strategies, and a minimal no_std program into the final link.

[The World of Linkers—Theory 15] Beyond ELF: The Rules Change with the File Format

Compare Mach-O and PE/COFF through their headers, directory records and contents. Byte layouts and coordinate conversions explain imports, pointer chains, unwind metadata, duplicate selection and TLS, while separating file structure, link-time interfaces and runtime contracts.

[The World of Linkers—Theory 16] From a Working Linker to One You Can Trust

A linker must do more than produce a runnable file. Follow the engineering behind parallel passes, deterministic output, useful diagnostics, layered tests, and incremental linking—and see how each optimization changes what must be proved.

Labs

[The World of Linkers—Lab 00] Distrust the Input: The ELF Boundary

Check byte ranges, the supported ELF64 header, and section-header indexes to establish an input boundary without arithmetic wraparound or out-of-bounds reads.

[The World of Linkers—Lab 01] Sections and Names: Carving Bytes into Regions

Decode section descriptors, borrow payload bytes, and resolve names so later stages consume named Sections instead of rereading raw fields.

[The World of Linkers—Lab 02] Symbols and Relocations: An Object File's Unfinished Business

Decode symbols and explicit-addend relocations, establish table ownership, and preserve information for definition selection and layout.

[The World of Linkers—Lab 03] Who Owns the Name? Global Symbol Resolution

Select global definitions before binding used references, preserving local identity, weak tie order, and COMMON requirements without assigning addresses.

[The World of Linkers—Lab 04] The First Executable: Two Program Headers and an Entry Point

Four stages have only read input. This lab writes output for the first time: plan a minimal executable image, serialize the ELF header and two program headers by hand, and let the Linux kernel run it and return 42.

[The World of Linkers—Lab 05] Calls Across Files: Merge the Code, Then Fill In the Displacements

The previous executable could come only from one object with no relocations. This lab lets an assembly entry point call a C function compiled in another file: merge code sections, compute S+A-P for every field, and never leave a half-written field behind on failure.

[The World of Linkers—Lab 06] File Bytes, Memory Bytes, and Segment Permissions

A code-only program is of little use. This lab adds read-only constants, initialized globals, BSS, and COMMON storage, describes them with three load segments of different permissions, and lets the kernel check the result: writing constants or executing data must end in SIGSEGV.

[The World of Linkers—Lab 07] Archives on Demand: A Fixed Point of Member Extraction

A static library is a container of object files, not one larger object. This lab parses the ar format, extracts members one at a time for unresolved global names, and rescans each archive until no further member is selected.

[The World of Linkers—Lab 08] Keep What Is Reachable: GC and Link Maps

Trace live sections from the entry and explicit roots, build an address-independent link plan, and emit deterministic byte-safe maps.

[The World of Linkers—Lab 09] Link Before the Load Address Is Known

Build a static PIE from class8's frontend and class6's layout, separating static fixups from load-time pointers repaired by supplied startup code.

[The World of Linkers—Lab 10] Rewrite Instead of Copy: Constants and Unwind Tables

Extend class8's ET_EXEC backend with merged constants, input-offset mappings, and rebuilt eh_frame records and search indexes for live code.

[The World of Linkers—Lab 11] Seeing Through Indirection: GOT, Relaxation, and Static TLS

Extend class9's static PIE with GOT slots, the supported mov relaxation, a TLS template, and supplied startup code that installs main-thread TLS.

[The World of Linkers—Lab 12] Make Debug Information Follow Addresses

Add debug sections, symbols, and section headers to ET_EXEC and static PIE, keeping image, debug-section, and TLS coordinates distinct.

[The World of Linkers—Lab 13] Addresses Have Semantics: Safe ICF and Code Order

Add conservative ICF and text ordering to class12's ET_EXEC and static-PIE backends, redirect references, and adjust debug information for folded copies.

[The World of Linkers—Lab 14] Take the Linker Call from rustc

Add a command-line driver to the cumulative class13 static-PIE backend and let rustc use it to link a no_std program that exercises core::fmt.

[The World of Linkers—Lab 15] Let Counterexamples Set the Quality Bar

Build replayable mutation and panic-survey tools, then test earlier backends through native execution, determinism checks, and behavioral comparison with GNU ld.