5.3.4 · D5Build Systems & Toolchain

Question bank — Dynamic - shared libraries — .so - .dll, dynamic linking, PIC

1,703 words8 min readBack to topic

Before the traps, one refresher so every term below is earned:

Recall The four objects these traps revolve around
  • .so / .dll ::: one file of machine code on disk, loaded into RAM and shared by many running programs.
  • PIC ::: code that uses only relative addresses, so it runs correctly at any load address.
  • GOT ::: a writable data table of real addresses, patched per process by the loader.
  • PLT ::: stubs that resolve a function's address on its first call (lazy binding), then cache it in the GOT.

The single picture behind every trap on this page — study it once before you start, then use it to check each answer:

Figure — Dynamic - shared libraries — .so  -  .dll, dynamic linking, PIC

True or false — justify

A shared library saves RAM only because it saves disk space.
False — the big win is RAM: the OS maps the same physical code pages into many processes at once, so disk saving and RAM saving are separate benefits.
The identical .so code bytes are mapped at the identical virtual address in every process.
False — the bytes are identical but the virtual address differs per process (that's exactly why PIC is required); what is shared is the physical page, not the address.
Because a shared library's code is read-only, the GOT must also be read-only.
False — the GOT is writable precisely so the loader can patch per-process addresses into it; keeping it separate from the code is what lets the code stay constant and shareable.
Static linking always makes a program start faster and run faster.
Half-true and misleading — one isolated process may start marginally faster, but system-wide, shared code pages are often already cached, so dynamic linking usually wins on total RAM and page-cache pressure.
-fPIC changes the observable behaviour of your program's output.
False — it changes how addresses are computed (relative + GOT indirection) but not what the code does; it's an addressing strategy, not a logic change.
Lazy binding means the whole library is loaded only when first used.
False — the library is mmapped at startup; laziness applies to symbol resolution of individual functions, resolved on their first call, not to loading the file itself.
If the linker accepts gcc app.o -lfoo, the program is guaranteed to run.
False — link time only verifies the symbol exists and records the dependency name; run time still has to physically find libfoo.so in the search paths, which can fail.
On Windows, every non-static function is automatically exported like on ELF.
False — Windows requires explicit __declspec(dllexport); ELF exports non-static symbols by default, so the two systems have opposite defaults.
PIC and ASLR are two names for the same mechanism.
False — PIC is a property of the code (relative addressing); ASLR is an OS policy that randomises the load address (so the black page in the picture lands somewhere unpredictable). ASLR depends on PIC/relocatable code but is not the same thing — the deeper story lives in ASLR & Security.

Spot the error

"The GOT stores the offsets to functions, computed at compile time and baked into the file."
Error: the GOT stores real absolute addresses filled in at load/run time per process (the red slots in the picture), not compile-time offsets; the PC-relative offset to the GOT is the compile-time part.
"The PLT resolves a symbol on every call, which is why dynamic linking is slow."
Error: the PLT resolves only on the first call and caches the result in the GOT; subsequent calls jump straight through the filled slot with no resolution.
"Since libc is shared, patching a bug in it requires relinking every program that uses it."
Error: dynamic linking's whole point is you patch libc.so once and programs pick it up on next launch without relinking — relinking-the-world is the static problem.
"A pure absolute call 0x401000 inside a .so is fine because the loader fixes the address."
Error: an absolute address baked into a code page would differ per process, breaking sharing (a red value on the black page); PIC forbids it — absolute addresses live only in the GOT (a data page).
"LD_LIBRARY_PATH is checked at compile time to locate the library."
Error: it's a run-time search path used by the dynamic linker ld-linux.so; the compiler/linker never consults it. Symbol existence is a link-time job, covered in The Linker — symbol resolution & relocation; finding the file is a separate run-time search.
"ELF uses PIC and Windows uses PIC too, they're identical."
Error: ELF leans on PIC by default; Windows DLLs historically preferred a fixed base address and used rebasing/relocation when taken — same problem, different default solution.

Why questions

Why can't the per-process real address of printf live inside the code page?
Because shared code bytes must be identical for all processes; any per-process-varying value (a red slot) must live in a per-process data page — the GOT — instead.
Why is lazy binding a default rather than always resolving everything up front?
Most programs call only a fraction of a library's functions, so resolving all symbols at startup would waste time and memory on symbols never used.
Why does dynamic linking add an "interpreter" (ld-linux.so) to the executable?
The kernel loads the executable but cannot itself resolve external symbols or map dependencies, so it hands control to the dynamic linker named in the file's interpreter field.
Why must GOT and PLT exist instead of just using PC-relative jumps for everything?
PC-relative addressing handles internal jumps easily, but external symbols (in another .so) have addresses unknown until load time — the GOT/PLT provide the indirection needed to fill them in later.
Why is separating a constant code page from a writable GOT the logical consequence of "share the code"?
If code must be byte-identical across processes it can hold no per-process value; the only place left for per-process addresses is a writable data page, which is the GOT — so the split (black vs red in the picture) is forced, not arbitrary.
Why does static linking waste the page cache system-wide?
Each static binary holds its own duplicate copy of the library code, so the cache stores many identical copies instead of one shared set of pages. The opposite trade-off (fat, self-contained binaries) is covered in Static libraries — .a .lib.
Why does the loader use mmap to bring library segments into a process?
mmap is the OS call that maps the same physical pages into multiple address spaces (exactly the sharing shown in the picture) and loads pages on demand; the mechanism itself is detailed in Virtual Memory & mmap.

Edge cases

What happens if a program uses dlopen() to load a library it never linked against at compile time?
It works — dlopen resolves the file and symbols entirely at run time, which is how plugins load code unknown at build time; that whole pattern is dlopen and Plugin Architectures.
What happens when two loaded libraries both export a symbol with the same name?
The dynamic linker resolves to the first definition found in its search order, so later definitions are shadowed — a classic source of subtle "wrong function called" bugs.
What if ASLR maps a DLL somewhere other than its preferred base address?
The Windows loader performs rebasing/relocation, patching the affected addresses; ELF PIC libraries need no such patching because they were already position-independent (the black page runs anywhere).
What happens if libfoo.so exists but a newer incompatible version replaces it?
The program may fail or crash at run time even though it linked fine earlier, which is why libraries carry version SONAMEs so incompatible versions don't silently substitute.
Can a shared library itself depend on other shared libraries?
Yes — its own DT_NEEDED entries list dependencies, and the dynamic linker recursively loads and resolves the whole graph before main runs.
What is the boundary case where dynamic linking gives no RAM benefit?
When only a single process on the whole machine uses the library — there's nothing to share the black page with, so you pay the indirection cost without the sharing payoff (though disk/update benefits remain).
What happens to lazy binding under a security hardening flag like full RELRO?
The loader resolves all symbols at startup and makes the GOT read-only afterward, trading lazy-binding speed for protection against GOT-overwrite attacks — part of the hardening story in ASLR & Security.