The C++ build pipeline runs every program through three distinct stages between your source code and a running process: compilation turns each .cpp file into an object file with placeholder symbols, linking combines those object files and libraries into one executable with real addresses, and loading is what the operating system does at runtime to map that executable into memory and resolve any remaining dynamic symbols. This article compares the three stages side-by-side, shows what each transforms, and maps common build errors to the stage that produced them. The deeper dives on each stage live in dedicated articles.
The three stages, at a glance
| Stage | When | Input | Output | Owns | Typical errors |
|---|---|---|---|---|---|
| Compilation | Build time (per file) | One .cpp translation unit | One .o object file | Preprocessing, parsing, template instantiation, codegen, optimization | error: 'foo' was not declared, template errors, syntax errors |
| Linking | Build time (per executable) | All .o files + static/dynamic libs | One executable or shared library | Symbol resolution, relocation, section merging | undefined reference to 'foo', multiple-definition errors, missing libraries |
| Loading | Run time (per launch) | An executable + its dependencies | A process running at main() | Mapping segments into the address space, dynamic-linker symbol resolution, constructor execution | error while loading shared libraries, version 'GLIBC_X.Y' not found, symbol lookup error |
Two rules of thumb:
- Compilation errors talk about your code. They reference line numbers and identifiers from your source.
- Linking and loading errors talk about symbols. They reference mangled or unmangled names with no file/line — because by then the compiler is gone and only the symbol table remains.
Compilation: source → object file
Each .cpp translation unit is processed independently. The compiler preprocesses (#include, #define), parses to an AST, instantiates templates, applies optimization passes, and emits machine code into an object file (.o on Linux/macOS, .obj on Windows). The object file contains the compiled code plus a symbol table that lists every symbol this translation unit defines and every external symbol it references.
Crucially, the compiler doesn't know where any external symbol lives in memory. Calls to std::vector::push_back or to functions defined in other translation units leave relocations in the object file — placeholders that say "the linker will fill this in." This is why compilation can succeed independently for every source file even if your program won't link.
Deep dive: The C++ Compilation Process.
Linking: object files + libraries → executable
The linker reads every object file plus any static libraries (.a) and shared libraries (.so / .dll) listed on its command line. For each external symbol referenced in an object file, the linker scans the others until it finds a definition, then patches the relocation with the real address (for static linking) or a stub that will be resolved at load time (for dynamic linking).
Linking is also where sections are merged: .text from every object file concatenates into the final .text, the symbol tables merge, debug info is rewritten, and constructors are wired into the program's initialization list.
Static vs dynamic linking is the major split:
- Static linking copies library code directly into the executable. The binary is self-contained but larger, and library bug fixes require a rebuild.
- Dynamic linking leaves library calls as stubs and defers resolution to load time. The binary is smaller and shares code with other processes using the same
.so, but launches more slowly and is sensitive to library version drift.
Deep dive: C++ Linking in Depth.
Loading: executable → running process
When you run ./my-program, the operating system's loader (or the dynamic linker ld.so for dynamically linked binaries) does several things before main() runs:
- Parses the executable's headers (ELF on Linux, Mach-O on macOS, PE on Windows) to find each segment.
- Maps segments into the process address space using
mmap—.textread-only and executable,.rodataread-only,.dataand.bssread-write. - Loads required shared libraries by recursively reading their headers and mapping their segments. The linker order matters: each
.solists its own dependencies. - Resolves dynamic symbols by patching the GOT (Global Offset Table) and PLT (Procedure Linkage Table). Lazy binding defers per-function resolution until the first call.
- Runs constructors in dependency order: global C++ objects' constructors, then the special
_initfunction, then__attribute__((constructor))functions. - Jumps to
main.
Most loader errors surface here — missing shared library, glibc version mismatch, ABI incompatibility, constructor SIGSEGV.
Deep dive: C++ Loading at Runtime.
Which stage caused this error?
A useful shortcut for triaging build errors:
| If the error says... | The stage is... |
|---|---|
error: 'foo' was not declared in this scope | Compilation |
error: invalid use of incomplete type | Compilation |
| Pages of template error messages | Compilation |
undefined reference to 'foo' | Linking |
multiple definition of 'foo' | Linking |
cannot find -lboost_system | Linking |
error while loading shared libraries: libfoo.so: cannot open | Loading |
version 'GLIBC_2.34' not found | Loading |
symbol lookup error: ./app: undefined symbol | Loading |
If the error references a line number, it's compilation. If it references a symbol but no file, it's linking. If it shows up only when you run the program, it's loading.
Reading further
The three deep dives go far past this overview:
- The C++ Compilation Process — preprocessing, AST, template instantiation, codegen, optimization passes.
- C++ Linking in Depth — symbol resolution, relocation, static vs dynamic, LTO, name mangling.
- C++ Loading at Runtime — ELF, the dynamic linker, GOT/PLT, lazy binding, constructor ordering.
