5.3.5 · D5Build Systems & Toolchain

Question bank — Symbol resolution — order matters

2,086 words9 min readBack to topic

We lean on four ideas everywhere below, so we pin them first — nothing after this uses a symbol or term that isn't nailed down here.


True or false — justify

The traditional Unix linker may go back and re-read an archive it already passed if a later object needs one of its members.
False. It scans left→right in a single pass; an archive is searched only against the that exists when the linker arrives at it. Later-created undefineds cannot revive a passed archive — that is exactly why order matters.
Reordering shared libraries (.so) on the link line never changes anything.
False. It usually seems harmless because the dynamic linker resolves against the whole .so at load time, but the static link still records dependencies in scan order, and --as-needed will drop a .so listed before the object that uses it.
Putting every library before every object is a safe, tidy default.
False, it is backwards. When each archive is scanned, is still empty (no object has run yet), so no archive member is pulled in — the archives contribute nothing. Objects first, libraries last.
If a symbol is undefined at the end of the scan, that is always a hard error.
True for a normal executable link — a non-empty at the end means undefined reference. (Exceptions like weak-undefined symbols resolve to null rather than error, but a plain strong reference must be satisfied.)
An archive member that defines a symbol nobody references still gets linked into the executable.
False. Archives pull in only members that satisfy a symbol currently in . An unreferenced member is skipped entirely — see Static vs Shared Libraries for why archives behave as an on-demand pool.
--start-group ... --end-group makes the link faster because it batches the archives.
False, it makes it slower on purpose. It forces the linker to re-scan the enclosed archives repeatedly until a full pass pulls in nothing, defeating the one-pass optimisation to solve circular dependencies.
Two source files each defining int count; (no initialiser) will always fail to link.
False on older toolchains — these are tentative (common) definitions and were merged silently into one storage. With -fno-common (now the default on modern compilers) it becomes a multiple-definition error.
If the order rule is obeyed, the same set of objects always lands in the executable regardless of whether libraries are static or shared.
False. A static archive contributes only the members you actually pull in; a shared library is a single load-time dependency (nothing is copied in at link). So the resulting binary's contents differ by design.

Spot the error

gcc -lm main.o — why does this fail even though libm clearly contains sqrt?
When -lm is scanned, is empty, so sqrt.o is not pulled in; main.o (scanned later) then makes sqrt undefined, but libm is already behind the scan cursor. Fix: gcc main.o -lm.
gcc main.o -lbar -lfoo fails with an undefined symbol from libfoo, but the symbol truly exists in libfoo. What is wrong?
-lbar pulled in a member that references a new libfoo symbol, but libfoo was scanned before that new undefined appeared — so it's never satisfied. This is a circular/ordering error; fix by -lfoo -lbar -lfoo or wrapping both in --start-group/--end-group.
A build links -lutil where libutil.a holds b.o with an __attribute__((constructor)), yet the constructor never runs. What's the mistake?
main.o references no symbol in b.o, so b.o is never pulled from the archive — a constructor with no referenced symbol is invisible to the on-demand rule. Fix: link b.o directly or use --whole-archive. See Library Constructors and __attribute__((constructor)).
Someone declares a global in a header as int total = 0; and includes it in three .c files, then gets a multiple-definition error under -fno-common. Where's the mistake?
A header defining a strong symbol gets that definition compiled into every including TU → many strong definitions → error. Fix: extern int total; in the header, int total = 0; in exactly one .c.
A C program calls a C++ library function and the linker reports undefined reference to 'do_work' even though do_work is compiled in. What's likely going on?
The C++ definition was name-mangled to something like _Z7do_workv, so the C caller's request for the plain do_work never matches. Fix: extern "C" on the definition. See Name Mangling in C++.
A link that worked yesterday breaks after someone alphabetically sorted the -l flags in the Makefile. Was the alphabetical order the real bug?
No — the real fragility was already there. The dependency order (user before provider) happened to coincide with the old order; sorting merely exposed that the link line was never expressed as a true dependency ordering.

Why questions

Why is "position on the link line part of the program's meaning" rather than a mere convenience?
Because whether a symbol resolves depends on the state of at the exact moment each input is scanned — moving an archive changes what contains when it is searched, which changes which members (and thus which code) end up in the binary.
Why was the one-pass, no-backtracking design chosen historically?
Archives lived on tape/disk where re-scanning was expensive, so the linker streamed through inputs once. A performance choice hardened into a semantic rule programmers must now obey — see The Linker (ld) and Linking Phases.
Why must an archive be searched repeatedly internally (until a pass adds nothing) even in single-pass linking?
Pulling in member A can introduce new undefineds that are satisfied by member B in the same archive. Without repeated internal passes, B (earlier in the archive but needed by A) would be missed. This repeat is within one archive, not across the whole line.
Why does --whole-archive exist if archives are supposed to link only what's needed?
Because "needed" is defined purely by referenced symbols, which misses code with side effects only — constructors, self-registering plugins, __attribute__((constructor)). --whole-archive includes every member to preserve those effects.
Why does the strong-vs-weak rule ("one strong wins over many weak silently") make bugs hard to find?
There is no diagnostic — the strong definition is chosen quietly, so a stray weak/tentative definition can be shadowed and the program links and runs while using the wrong storage. Nothing tells you a choice was made.
Why does putting a library both before and after the objects (-lfoo main.o -lfoo) sometimes "fix" a broken build?
The trailing -lfoo is searched against the produced by main.o, so it can satisfy those references. The leading one does nothing (empty ). It works but is a smell — the correct expression is objects first, -lfoo after.
Why can the ELF symbol table tell you which file will win a resolution, but not always why?
The table lists each symbol's binding (global/weak) and whether it is defined or undefined, so you can see candidate providers; but the winner depends on scan order and the linker's strong-over-weak policy, which the table alone doesn't encode. See Object File Format (ELF) and Symbol Tables.

Edge cases

If is already empty when the linker reaches the very first library, what happens to that library?
Nothing is pulled from it — no member satisfies a current undefined, so it is effectively skipped. This is the "library too early = empty = useless" failure.
What happens when the same archive appears twice on the link line with different states each time?
Each occurrence is searched against as it stands at that point, so the second appearance can legitimately pull in members the first one skipped. That is precisely the manual fix for two-library cycles.
What happens if an object file references a symbol that no input ever defines?
It stays in through the whole scan; at the end, non-empty triggers undefined reference. Order cannot help — the definition simply doesn't exist anywhere.
If two archives both contain a member defining the same symbol, which one supplies it?
The first archive reached (left→right) that still has the symbol in wins; once pulled, the symbol moves from to and the later archive's member is never requested. Order decides the provider silently.
A member defines symbol X (needed) and also symbol Y (a duplicate strong definition of something already in ). What happens when it's pulled in for X?
Pulling the member to satisfy X drags Y along too; since Y is a second strong definition of a name already in , you get a multiple-definition error even though you only wanted X. On-demand inclusion is per-member, not per-symbol.
Does --as-needed change which static archive members get linked, or only shared libraries?
It targets shared libraries — it drops a .so that ended up contributing no needed symbols (typically one listed before its user). Static archive member selection already follows the on-demand rule independently.
If every symbol resolves but a needed constructor's member was never pulled, is the link "correct"?
It links with empty , so the linker calls it correct — yet the program is semantically broken because required initialisation code was silently omitted. Correct linking correct program when side-effect-only code is involved.

Connections