Video summary

Linkers, Loaders and Shared Libraries in Windows, Linux, and C++ - Ofek Shilon - CppCon 2023

Main summary

Key takeaways

Technology

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/delete variants).

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:

  • /DELAYLOAD with 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-undefined being 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)
  • 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

Original video