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:
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 processWhat 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:
$ 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 putsCompilation 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:
- Resolve every
Uentry against aT(or data) definition. If a symbol is unresolved, you getundefined 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. - 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
| Stage | Typical error | What it means |
|---|---|---|
| preprocessor | foo.h: No such file | include path, not logic |
| compiler | expected ';' before ... | syntax/type; no object emitted |
| assembler | operand size mismatch | bad inline asm or mnemonics |
| linker | undefined reference to, multiple definition of | a symbol is missing or defined twice |
| loader | error while loading shared libraries | a NEEDED .so absent or wrong ABI |
| runtime | command not found, wrong results | PATH, 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.
nmon 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
NEEDEDentries — 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 / Boundary | Typical Latency | Dominant Bottleneck | Production Mitigation |
|---|---|---|---|
| L1 / L2 Cache Access | 1 ns – 4 ns | Register dependency chains & pointer chasing | Contiguous memory layouts, cache-line alignment (64 B), and batch iteration |
| Main Memory (DRAM) Fetch | 60 ns – 100 ns | TLB misses, random heap indirection, NUMA hops | HugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers |
| Kernel Syscall / Context Switch | 1 µs – 5 µs | User-to-kernel mode switch, page-table flush, lock contention | Non-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O |
| NVMe SSD Random Read / fsync | 50 µs – 500 µs | Block I/O queue depth, write amplification, WAL flushes | Group commit, sequential append logs, and bounded buffer-pool pinning |
| Intra-Region RPC Roundtrip | 0.5 ms – 2.5 ms | Serialization, 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:
- 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.
- How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized
100 msslowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early. - 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.