Zack Li logoZack Li

Back to Blog

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 runsload → call.
  • C++ only (C has none of these):
    • virtual : a call on a Base* runs the Derived override, chosen at run time via the vtable
    • class : object-oriented programming
    • inheritance : 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.

  1. Direct : C and C++. You write foo(). The compiler already knows it is foo — no lookup at run time.
void foo(void);
foo();
  1. Function pointer : C and C++. You store a function in a variable, then call whatever is in that variable.
void (*fp)(void) = foo;
fp();
  1. 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();
  • 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 (UT).
    • nm — CLI that lists symbols in a .o / exe / DSO (nm a.o). U and T are its type letters.
    • U — undefined: this .o calls 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
FileWhere foo livesWhen it is mappedWhen to use
Static link.a (zip of .o)copied into the exeApp runs — already in the process. No extra loadShip one file
Dynamic link.so / .dylib / .dllstays in the DSOLoader maps it at process start, before main. No dlopenShare one lib across apps
Dynamic loadsame DSO, path at run timestays in the DSOdlopen after the app has startedPlugins

No static loading. A static .a was already copied in at link time.

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

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.

  1. dlopen(path) — map file → handle. Does not call foo.
  2. 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

  1. CMake writes the recipe
  2. Ninja/Make/MSBuild runs it
  3. g++ / clang++ / cl compiles, 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
  • configurecmake writes the recipe, then exits. Does not build your app.
  • native build — Ninja / Make / MSBuild invokes commands. Does not parse C++.
  • compiler / toolchaing++ / clang++ / cl compiles .cpp.o, then starts ld / lld / link.
TermCategoryOSProperties
CMakeconfigureallWrites build.ninja / Makefile / .sln
Ninjanative buildallRuns those commands. cmake --build starts Ninja/Make/MSBuild
LLVMcompiler / toolchainallBackend (IR, opt, codegen). Also lld, lldb. Not a g++ stand-in
Clang / clang++compiler / toolchainall; default macOSFrontend on LLVM. clang = C, clang++ = C++
GCC / g++compiler / toolchainall; default LinuxOwn backend, not LLVM. gcc = C, g++ = C++
MinGW-w64compiler / toolchainWindowsGCC port: still g++.exe / .dll. Not a generator
MSVC / clcompiler / toolchainWindowsDriver 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++

© Copyright 2026 Zack Li.