How C/C++ becomes a running process: compile, link, load
Created on June 30, 2026
This session explains compile and link: how a .c / .cpp becomes a relocatable .o, then an exe or DSO, then a process. It also introduces static link, dynamic link, and dynamic load (dlopen).
1. C vs C++
- Same pipeline:
.c/.cpp→ compile →.o→ link → exe or DSO → App runs → load → call. - C++ only (C has none of these):
virtual: a call on aBase*runs theDerivedoverride, chosen at run time via thevtableclass: object-oriented programminginheritance: derived classes inherit from a base class
.c / .cpp
│ compile
▼
.o
│ link
▼
exe or DSO
│ 1. App runs exe → OS starts a new process
▼
process
│ 2. load DSO → mapped into that same process
▼
process (exe + DSOs)
│ call
▼
function
├─ direct C and C++ target baked into the instruction
├─ function ptr C and C++ load addr, then call
└─ virtual C++ only object → vptr → vtable → override
- Order: App runs first. Then load. A DSO never starts its own process.
- exe — OS starts this as a process.
- DSO —
.dylib(macOS) ·.so(Linux) ·.dll(Windows). Loaded into the running process.
Three ways to call a function
C has 1 and 2. C++ has all three.
- Direct : C and C++. You write
foo(). The compiler already knows it isfoo— no lookup at run time.
void foo(void);
foo();
- Function pointer : C and C++. You store a function in a variable, then call whatever is in that variable.
void (*fp)(void) = foo;
fp();
- Virtual : C++ only. You still write a normal-looking call. The compiler turns it into a vtable lookup (a hidden function-pointer call). C cannot do this.
Backend* b = new GpuBackend();
b->run();
2. Build: compile + link
- Compile — one translation unit (
.c/.cpp+ its headers) → one.o. Not runnable. - Link —
.o/ libs → exe or DSO. Match each undefined name to a definition (U→T).nm— CLI that lists symbols in a.o/ exe / DSO (nm a.o).UandTare its type letters.U— undefined: this.ocalls it, does not define it.T— text/code: some.o/ lib defines it as a function.
- Build — compile + link. Make / Ninja / Bazel run those commands. CMake only generates the graph.
g++ -c a.cpp -o a.o # compile — does not look at b.cpp
g++ -c b.cpp -o b.o
g++ a.o b.o -o app # case 1: static linking → exe
g++ -shared -fPIC a.o b.o -o libfoo.so # case 2: dynamic linking → DSO
Static link, dynamic link, dynamic load
| File | Where foo lives | When it is mapped | When to use | |
|---|---|---|---|---|
| Static link | .a (zip of .o) | copied into the exe | App runs — already in the process. No extra load | Ship one file |
| Dynamic link | .so / .dylib / .dll | stays in the DSO | Loader maps it at process start, before main. No dlopen | Share one lib across apps |
| Dynamic load | same DSO, path at run time | stays in the DSO | dlopen after the app has started | Plugins |
No static loading. A static .a was already copied in at link time.
static link
Use STATIC link to copy foo into app at link time.
add_library(foo STATIC foo.cpp) # STATIC = .a, not a DSO
add_executable(app main.cpp)
target_link_libraries(app PRIVATE foo) # link foo → code copied into app
dynamic link
SHARED DSO; CMake records the name in app. Loader maps it before main.
add_library(foo SHARED foo.cpp) # SHARED = DSO (.so / .dylib / .dll)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE foo) # link foo → loader maps it at start
dynamic load
Same SHARED DSO, do not link it. Path at run time.
dlopen(path)— map file → handle. Does not callfoo.dlsym(handle, "foo")— name → address. Cast, then call (way 2).
add_library(plugin SHARED plugin.cpp) # build the plugin DSO
add_executable(app main.cpp)
# Do NOT write: target_link_libraries(app plugin)
# That would be dynamic *link* (loader maps plugin at start).
# Here app must find plugin.so itself via dlopen("plugin.so").
To call a function in the DSO:
void *h = dlopen("plugin.so", RTLD_NOW); // map
void *p = dlsym(h, "plugin_create"); // lookup
void *(*create)(void) = p;
create(); // call
3. CMake → native build → compiler
- CMake writes the recipe
- Ninja/Make/MSBuild runs it
g++/clang++/clcompiles, then starts the linker.
-G = who runs the recipe.
-DCMAKE_CXX_COMPILER= = who compiles. Separate flags.
CMakeLists.txt # targets + sources
│
▼
cmake -B build # configure only
│ -G generator (Ninja, Make, VS)
│ compiler PATH or -DCMAKE_CXX_COMPILER=
▼
build.ninja / Makefile / .sln # native recipe; CMake is done
│
▼
ninja / make / MSBuild # execute (or: cmake --build)
│
▼
g++ / clang++ / cl # compile + start linker
│ MinGW-w64 = this g++ on Windows
▼
.o → linker # ld / lld / link
│
▼
app
- configure —
cmakewrites the recipe, then exits. Does not build your app. - native build — Ninja / Make / MSBuild invokes commands. Does not parse C++.
- compiler / toolchain —
g++/clang++/clcompiles.cpp→.o, then startsld/lld/link.
| Term | Category | OS | Properties |
|---|---|---|---|
| CMake | configure | all | Writes build.ninja / Makefile / .sln |
| Ninja | native build | all | Runs those commands. cmake --build starts Ninja/Make/MSBuild |
| LLVM | compiler / toolchain | all | Backend (IR, opt, codegen). Also lld, lldb. Not a g++ stand-in |
Clang / clang++ | compiler / toolchain | all; default macOS | Frontend on LLVM. clang = C, clang++ = C++ |
GCC / g++ | compiler / toolchain | all; default Linux | Own backend, not LLVM. gcc = C, g++ = C++ |
| MinGW-w64 | compiler / toolchain | Windows | GCC port: still g++ → .exe / .dll. Not a generator |
MSVC / cl | compiler / toolchain | Windows | Driver cl, linker link. Own ABI. Often -G "Visual Studio" → MSBuild → cl |
g++ main.cpp -o app # no CMake, no Ninja
cmake -G Ninja -B build # Ninja + default compiler
cmake -G Ninja -B build -DCMAKE_CXX_COMPILER=clang++ # Ninja + clang++
cmake --build build # starts Ninja → that compiler → app
One toolchain per build tree. Change compiler → new directory. Do not mix MSVC .obj with MinGW/GCC .o.
cmake -G Ninja -B build-gcc -DCMAKE_CXX_COMPILER=g++
cmake -G Ninja -B build-clang -DCMAKE_CXX_COMPILER=clang++