14.3 Static Linking vs Dynamic Linking
After resolving object-file relationships, a toolchain must decide when library code becomes part of the program. Static linking copies selected code and data from relocatable objects or archives into the final executable. Dynamic linking leaves dependencies on shared objects to be mapped and connected by the runtime loader. Neither strategy is universally better; each moves costs and responsibilities among build time, deployment, startup, memory sharing, optimization, compatibility, and security updates.
A static archive such as libparse.a is usually an indexed collection of .o members, not one already-linked module. The linker scans its symbol index and extracts a member only when that member defines a currently unresolved symbol. This demand-driven behavior prevents every utility function in a large archive from entering the executable.
Archive extraction is stateful and order-sensitive
Assume main.o needs parse; parse.o in libA.a defines parse but needs scan; and scan.o in libB.a defines scan but refers back to parse. In a traditional left-to-right link, the linker maintains sets of defined and unresolved symbols. When it visits an archive, it repeatedly extracts useful members from that archive, but it may not automatically return to an archive already passed.
cc main.o -lA -lB # parse.o is extracted, then scan.o: succeeds
cc main.o -lB -lA # libB is initially irrelevant; scan remains unresolvedCircular archive dependencies may require repeating libraries, using a linker group that rescans until no member is added, or reorganizing the libraries to remove the cycle. --whole-archive forces all members in, which is appropriate for plugin registries driven only by constructors or metadata but increases size and may introduce duplicate symbols. The diagnostic lesson is important: command-line order is part of the static link algorithm, not cosmetic spelling.
Choosing where to pay the cost
A fully static executable can be copied as one artifact and is insulated from most target-machine library drift. The linker can garbage-collect unused sections and, with link-time optimization (LTO), optimize across module boundaries. The tradeoff is duplicated library code across executables or processes, larger updates when one library changes, and complications for facilities that expect runtime modules, name-service plugins, or platform frameworks. “Static” also does not automatically mean hermetic: configuration files, kernel interfaces, certificates, locale data, and dynamically loaded resources may remain external.
A dynamically linked executable is smaller and multiple processes can share clean code pages from the same shared library. A security update to a compatible library may fix many programs without relinking them. In return, deployment must provide the right library names, search paths, architectures, symbol versions, and ABI. An API describes source-level calls; an ABI includes binary layout, calling convention, name and version rules, and object format. A source-compatible change can still be ABI-breaking.
Loading and binding shared objects
An executable records dependencies, often as ELF DT_NEEDED entries. At startup, the loader maps those shared objects, recursively discovers their dependencies, applies required relocations, resolves symbols according to scope and version rules, initializes thread-local storage, applies final page permissions, and runs constructors before transferring control to main.
Binding may be eager, resolving relevant imports before execution, or lazy, deferring a function's resolution until its first call through a PLT/GOT mechanism. Eager binding moves work to startup and lets full RELRO close writable relocation tables earlier. Lazy binding can shorten initial startup but makes the first call slower and historically requires a writable patch window. Modern systems choose different defaults and hardening policies.
Deployment decisions should be measured against a concrete product. A tiny recovery tool may value a self-contained static binary. A desktop application may rely on system frameworks. A containerized service may use dynamic libraries inside an immutable image. A fleet with many worker processes may benefit from shared pages, while a serverless function may prioritize cold-start latency. Record artifact size, proportional set size, startup profile, update bandwidth, ABI policy, reproducibility, license constraints, and incident-repair procedure. “Static versus dynamic” is not merely a compiler flag; it is a lifecycle design.