Video summary
Linkers, Loaders and Shared Libraries in Windows, Linux, and C++ - Ofek Shilon - CppCon 2023
Main summary
Key takeaways
Summary (tech focus: linkers, loaders, shared libraries on Windows vs Linux; key C++ implications)
The talk explains how shared libraries and executables are connected at build time (linking) and at runtime (loading), focusing on how symbol resolution and relocation enable features like interposition and lazy binding. It contrasts Windows and Linux design choices and derives developer-facing recommendations, especially for C++ shared libraries.
1) End-to-end build + runtime model: object files → linked binaries → memory mapping
Compilation: object files and sections
Compilation produces object files containing sections (notably):
.text: machine code.data: program data
Linking: merging and layout on disk
During linking:
- identically named sections across object files are merged into larger sections
- sections are reordered on disk to be laid out with compatible runtime permissions adjacent
Loading: segment mapping and page permissions
At runtime, the loader:
- maps adjacent on-disk chunks called segments into memory
- applies page-aligned mapping
- then sets page permissions (e.g., code pages become read+execute)
2) How calls become “wired”: relocations and indirection
A common pattern: code calls a function Fu, but the call target address is not known at static link time.
Relocations as loader “to-do items”
Relocations create entries for the loader:
- Direct/text relocations patch the call site (the placeholder becomes the real address)
This approach is “frowned upon” because it:
- makes code pages unsharable across processes
- requires per-call-site fixing (potentially huge overhead if a function has many call sites)
Indirection via a single placeholder slot (more typical)
A more common mechanism:
- the loader fills one slot with the function’s address
- all call sites indirect through it
Tradeoff:
- adds runtime indirection cost
- improves load-time behavior and memory sharing
3) Windows vs Linux: how they describe imports/undefined symbols to the loader
Windows (IAT-centric model)
A Windows binary includes an Import Data section (.idata) with a directory table:
- one entry per imported DLL
- each entry includes:
- DLL name
- mapping info to locate imported symbols
- offset into the Import Address Table (IAT) where the loader writes resolved addresses
Developer implication: cross-binary calls are typically indirect through IAT, with overhead similar to vtable-like dispatch.
Linux (ELF-centric model)
Linux uses two related concepts:
- dynamic section: raw list of library names to map
- symbol table: includes imported symbols marked undefined at link time
The loader searches for undefined symbols across libraries and wires them.
Far-reaching implication: a symbol may resolve to a different implementation than originally intended, enabling interposition.
4) Interposition: overriding symbols across binaries (core Linux behavior)
What interposition is
Interposition means the loader can redirect a symbol reference in one binary to an overriding definition found in another binary.
The speaker discusses a motivation hypothesis: ELF designers may have assumed the ubiquity of something like libc, making it useful for users to override its behavior.
Platform contrast
- Windows: effectively does not allow overriding a shared-library symbol from the executable (in this framing)
- Linux: yes, due to interposition design
- macOS: “yes but not by default” (requires non-default switches)
Loader symbol search order (breadth-first)
Linux performs a breadth-first library search across the dependency graph.
Important consequence: libraries reached “later” (deeper dependencies) can still be overridden, because earlier nodes (executable, then earlier dependencies) are searched first.
How to change resolution
- Linker switch example:
-B symbolic - Environment:
LD_PRELOAD- loads specified libraries after the executable but before dependencies
- affects what gets chosen during interposition
5) C++ connection: interposition relates to operator new replacement
The speaker points to a niche where C++ standard wording intersects with interposition:
- C++ allows a program to provide definitions for dynamic allocation functions (various
new/deletevariants).
Claim (per speaker framing):
- Linux can support this via interposition
- Windows does not conform to that standard clause in the strict sense
ISO mailing list idea
A standard-evolution suggestion discussed on an ISO mailing list:
- instead of relying on interposition wording,
- require a library-provided override hook (e.g., a hypothetical “set-new-override” mechanism),
- similar in spirit to standardized handler hooks.
6) --allow-shlib-undefined / undefined symbol checking control
Linux link behavior (as described):
- For an executable: undefined symbols must be resolved at link time (otherwise link fails)
- For a shared library: link may succeed even if some undefined symbols can’t be resolved until runtime
This behavior can be modified with specific linker switches (the speaker mentions defaults and “allow undefined” style controls).
7) Process-wide singleton and circular dependencies consequences
Singleton pattern
- Windows: typically requires linking all code against the same shared library that owns the singleton instance
- Linux: singleton can naturally become process-wide if:
- the singleton is implemented in the executable
- shared libraries reference it
- loader resolution selects the right instance
Circular dependencies
- Linux: circular shared-library dependencies can work (runtime resolution)
- Windows: “by design” not straightforward; circular dependencies need major hacks
The speaker notes circular dependencies can be convenient but may encourage sloppy architecture.
8) Deeper mechanics: PIC/PIE, GOT/PLT, and why interposition affects optimization
Position-independent code
- Not position independent: calls to hardcoded addresses are invalid if loaded elsewhere.
- Position independent: uses PC-relative addressing, but does not allow interposition hijacking in the same way.
Typical shared-library PIC approach:
- use indirection through the Global Offset Table (GOT)
- default behavior: calls indirectly through GOT, enabling interposition
Performance implication:
- indirect calls resemble virtual-call overhead
- interposition prevents many compiler assumptions → reduced inlining and interprocedural optimization
- toolchain nuance: clang handling differs, but is still framed as a cost
Executables vs PIE
To make an executable position-independent under ASLR, it needs PIE (speaker references the -fPIC vs executable flag distinction conceptually).
9) Lazy binding (Linux) vs delay load (Windows)
Motivation
Lazy binding postpones symbol resolution until the first call, improving startup time for large binaries that don’t use many symbols.
Linux: lazy binding is on by default
Mechanism:
- GOT entry initially points to a resolver path (via a procedure lookup stub)
- first call jumps into the resolver inside the loader
- resolver finds the function, overwrites the GOT slot, then transfers execution
- subsequent calls jump directly to the resolved function
Key terms:
- GOT: indirection target
- PLT: procedure lookup table stubs
Windows: lazy binding is off by default
The speaker calls it delay load; similar behavior requires:
/DELAYLOADwith the DLLs to delay-load
Security wrinkle: writeable GOT during lazy binding
Lazy binding requires writable GOT during execution → security risk (GOT overwrite attacks were referenced).
Many distros/linkers move toward:
- eager relocation at load time
- then making GOT/structures read-only
Mentioned behavior: PLT structure may still exist, but resolution happens eagerly (“we still erect this magnificent structure” despite avoiding lazy runtime writes).
10) Symbol visibility: the practical lever to reduce interposition costs
Windows
Visibility is simpler:
- symbols are DLL-exported or DLL-imported.
Linux
The symbol table always exists, and symbols can be controlled with attributes:
- default
- protected
- hidden
- (internal noted as noop)
How visibility maps to call mechanisms:
- hidden symbols take “shorter” resolution paths (less interposition opportunity)
- default symbols remain interposable and use the longer PLT/GOT route
Recommendations (speaker’s guidance)
When building shared libraries on Linux:
- opt out of interposition
- build with
-fvisibility=hidden - explicitly mark intended external API:
- visibility: default for stable public interface only
- otherwise protected (or not default) to prevent interposition
Goal:
- improve linking/load times
- reduce symbol clashes
- enable more optimized code
11) Takeaways / concluding positions
- Linux vs Windows designs differ fundamentally, especially around interposition and symbol resolution.
- Developer actions on Linux:
- use hidden visibility and careful symbol exporting
- “link more like Windows” to avoid interposition overheads and optimization loss
- C++ standard note:
- shared libraries are outside the scope only in the “implementation detail” sense; they still matter on real platforms.
Main speakers / sources
- Speaker: Ofek Shilon (CppCon 2023)
- Named external source/authority mentioned:
- Fang Jun / musr (main ld maintainer; quote referenced regarding
--allow-shlib-undefinedbeing an unfortunate default) - Thiago Matera (ISO mailing list suggestion: replace interposition wording with an override hook idea)
- System V ABI (ABI discussion regarding pointer-resolution corner cases)
- Fang Jun / musr (main ld maintainer; quote referenced regarding
- Compiler/toolchain references used in the talk:
- GCC, clang, ld/Gold-like linker behavior
- ELF, ASLR, LD_PRELOAD, dlopen
- mentions of Zig/Rust using eager relocation / RELRO-style approaches