TRT
Sign in
Operating Systems

From Source Files to a Running Program

A build turns source text and dependencies into an executable representation through distinct stages.

The Runtime Theory Team8 min read#compiler#linker#loader

A build turns source text and dependencies into an executable representation through distinct stages, and each stage answers a different question. Knowing which stage owns an error is most of debugging a build: a syntax error dies in the compiler, an undefined function dies in the linker, a missing shared library dies in the loader, and a wrong PATH dies at runtime. The pipeline, for a C-family language on Linux:

text
foo.c --[cpp: macros, includes]--> foo.i
foo.i --[cc1: parse, optimize]--> foo.s
foo.s --[as: encode]--> foo.o
foo.o + libbar.a + libc.so --[ld: resolve, relocate]--> a.out
a.out --[kernel execve + ld.so]--> running process

What an object file contains

gcc -c foo.c produces an ELF object file with no knowledge of main.c next to it. It holds sections (.text for code, .data for initialized globals, .bss for zeroed ones) and a symbol table split into defined and undefined entries:

bash
$ gcc -c foo.c && nm foo.o
0000000000000000 T add         # defined: this file defines add
                 U main        # undefined: nothing here defines main
                 U puts        # undefined: this file calls puts

Compilation is deliberately local: each translation unit is checked and lowered on its own, so a change to one file rebuilds only that file. All the cross-file knowledge is postponed to the next stage.

The linker resolves and relocates

The linker collects the object files and libraries, builds one global symbol table, and performs two jobs:

  1. Resolve every U entry against a T (or data) definition. If a symbol is unresolved, you get undefined reference to 'add' — the error that tells you a definition is missing, misnamed, or in a translation unit that never got compiled into the link.
  2. Relocate: patch each instruction's placeholder address now that the final layout is known. Position-independent code does this indirectly through the GOT/PLT, so the same code can sit anywhere in memory.

Two subtleties bite everyone at least once. A static library (.a) is an archive of objects pulled in only to satisfy an unresolved symbol at the moment it is scanned — put libfoo.a before the object that needs it on the command line and it is silently skipped. A dynamic dependency (.so) is recorded as a NEEDED entry and deferred entirely: ldd a.out lists them, and a version mismatch surfaces only when the program starts.

The loader takes over

Once execve succeeds, the kernel maps the executable's segments into a fresh address space, maps the interpreter (/lib64/ld-linux-x86-64.so.2), sets registers to the entry point, and hands over to the dynamic loader. It resolves NEEDED libraries, applies relocations, runs the global constructors of every loaded library (which is why static initializers run before your main), and only then jumps to the program's entry. Cost is real: cold-start mapping and relocating a large binary with dozens of shared libraries takes tens of milliseconds — the entire reason preloading and single-binary deployments exist.

Errors map to stages

StageTypical errorWhat it means
preprocessorfoo.h: No such fileinclude path, not logic
compilerexpected ';' before ...syntax/type; no object emitted
assembleroperand size mismatchbad inline asm or mnemonics
linkerundefined reference to, multiple definition ofa symbol is missing or defined twice
loadererror while loading shared librariesa NEEDED .so absent or wrong ABI
runtimecommand not found, wrong resultsPATH, search path, environment

Key takeaways

  • Compilation is per-file and local; linking is global and symbolic; loading is the OS mapping bytes and the dynamic linker patching them.
  • nm on an object file separates "I don't know this symbol" (normal, fixed by linking) from "this symbol does not exist anywhere" (fixed by adding a source file, not by editing flags).
  • Static archives are resolved by scan order, dynamic libraries by recorded NEEDED entries — the two produce completely different failure modes.
  • Constructors of loaded libraries run before main, so global initialization order across shared objects is a real, observable dependency.

Production cost model and latency numbers

To apply From Source Files to a Running Program effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:

Operation / BoundaryTypical LatencyDominant BottleneckProduction Mitigation
L1 / L2 Cache Access1 ns – 4 nsRegister dependency chains & pointer chasingContiguous memory layouts, cache-line alignment (64 B), and batch iteration
Main Memory (DRAM) Fetch60 ns – 100 nsTLB misses, random heap indirection, NUMA hopsHugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers
Kernel Syscall / Context Switch1 µs – 5 µsUser-to-kernel mode switch, page-table flush, lock contentionNon-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O
NVMe SSD Random Read / fsync50 µs – 500 µsBlock I/O queue depth, write amplification, WAL flushesGroup commit, sequential append logs, and bounded buffer-pool pinning
Intra-Region RPC Roundtrip0.5 ms – 2.5 msSerialization, TCP/TLS framing, tail queueing ($L = \lambda W$)Connection pooling, deadline propagation, and p95 hedged requests

Failure modes, debugging, and operational checklist

In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on From Source Files to a Running Program, walk through three verification questions:

  1. What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
  2. How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized 100 ms slowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early.
  3. Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save