5.3.3 · D5Build Systems & Toolchain

Question bank — Static libraries — .a - .lib, creation and linking

2,067 words9 min readBack to topic

The mechanics being probed: a static library is an ==archive of .o files plus a symbol index, and the linker copies only the members it needs, resolving left-to-right in a single pass==. Almost every trap here is a consequence of one of those two facts.


First, three pictures to anchor the vocabulary

Figure — Static libraries — .a  -  .lib, creation and linking

The figure above shows the drawer: three envelopes (add.o, sub.o, mul.o), the index card at the front listing which symbol lives in which envelope, and the rule that the linker grabs a whole envelope or nothing.

Figure — Static libraries — .a  -  .lib, creation and linking

Trace the figure left to right: main.o enters and puts add on the want-list (red). When the scan reaches -lmath, add is pending, so the index matches and add.o is pulled in (green), clearing the want-list. Reverse the order and the archive is scanned with an empty want-list — nothing matches, nothing is pulled, and add fails later.

Figure — Static libraries — .a  -  .lib, creation and linking

The figure shows the chain: pulling add.o introduces a new undefined symbol helper; the linker loops back to the index, finds helper in util.o, and pulls that too — all in one visit to the archive.

Figure — Static libraries — .a  -  .lib, creation and linking
aspect static .a / .lib shared .so / import .lib.dll
code lands in exe? yes, copied at build time no, only a reference is recorded
file needed at run time? no — delete it freely yes — the .so/.dll must be present
what -l/import provides the actual object member a stub pointing at the runtime library
mnemonic "copy, not reference" "reference, load later"

See Shared libraries — .so / .dll and ELF and PE file formats for how the run-time loader finds the second column's files.


True or false — justify

A .a file must be present on disk for the finished executable to run.
False — static linking copies the needed machine code into the executable at build time, so the archive is only a build-time dependency (unlike a `.so`/`.dll`).
Reordering main.o -lmath to -lmath main.o still links fine on GNU ld.
False — the linker keeps only pending undefined symbols; if -lmath comes first, nothing is undefined yet so it pulls nothing, then main.o references add and it's too late.
An archive built with ar rc (no s) can never be linked at all.
False — you can link it after running ranlib to add the index; the s flag just does that index step at creation time so the linker isn't stuck scanning members.
If a static library defines 50 functions and you call 1, all 50 end up in your executable.
False (classically) — the linker pulls only the member objects it needs; if each function sits in its own .o, unused ones are skipped, though everything inside a used member comes along.
ar compiles your source code into the archive.
False — ar is a dumb archiver; it just bundles already-compiled .o files. The compiling was done earlier by gcc -c (Compilation pipeline — preprocess, compile, assemble, link).
Two members of the same .a may define the same symbol without any problem.
False — that's a duplicate-definition conflict; if both members get pulled the linker reports a multiply-defined symbol, exactly as it would for two loose .o files.
Naming a file math.a lets you link it with -lmath.
False — -lNAME expands to lib<NAME>.a, so it searches for libmath.a; either rename the file or pass its full path directly.
On Windows a .lib always contains real machine code just like a Unix .a.
False — a MSVC .lib may be either a static library (real code) or an import library (tiny stubs pointing at a .dll); the second kind resolves names at link time but the code lives in the DLL loaded at run time.
A static library has an entry point (a main) like a program.
False — libraries are collections of relocatable code with no entry point; main lives in the program that links against the library.

Spot the error

gcc -lmath main.o -o app → what's wrong?
The library precedes its user, so no symbol is pending when -lmath is scanned and add stays undefined; put main.o before -lmath.
ar rc libmath.a add.o then a link fails with "archive has no index" — cause?
The s flag was omitted, so there's no symbol table; fix with ar rcs at creation or ranlib libmath.a afterward.
gcc -c add.c -o add.o but the file has no main — is that an error?
No error at all — -c stops before linking and produces a relocatable object, which is exactly what a library member should be; main is neither needed nor wanted here.
gcc main.c ./math.a -o app when the archive is named math.a — legal?
Yes — passing the full path sidesteps the -l naming convention entirely, so math.a works even without the lib prefix.
Cyclic dependency: -lA needs B, -lB needs A, listed once each — why might it fail?
A single left-to-right pass can leave one library's symbols still pending after the other was already scanned; wrap them in -Wl,--start-group -lA -lB -Wl,--end-group to re-scan.
nm libmath.a shows add marked U inside main.o's listing — is the library broken?
No — U means undefined/needed by that member, T means defined; main.o legitimately needs add, and some other member (or object) must provide the T.
Shipping only the app.exe but the program crashes at launch with "DLL not found" — what happened?
The .lib you linked was an import library, so real code stayed in a .dll that must ship alongside the exe — a shared-library trap, not a static one.

Why questions

Why does the linker only pull members that resolve a currently undefined symbol?
To satisfy selectivity — it avoids copying code nothing references, keeping the executable from bloating with unused functions.
Why must the library come after the objects that use it (Unix)?
GNU ld is single-pass and only remembers symbols left undefined so far; a reference appearing later than its provider was never on the wanted list when the archive was scanned.
Why is the symbol index (s) an optimization rather than raw necessity?
Without it the linker could still find symbols by scanning every member sequentially; the index is a fast lookup table (add → add.o) that makes resolution quick.
Why can order within one archive be forgiving while order across libraries is strict?
Because a single visit to an archive re-scans its own index whenever a pulled member introduces new undefined symbols, resolving internal chains in any order; but each separate -l is visited only once as the pass moves right, so cross-library dependencies must be ordered by hand.
Why is it called a static library rather than dynamic?
Because the binding of code into the program happens statically, at build time — contrast with dynamic/shared libraries bound at run time.
Why do library sources get compiled with -c (stop before linking)?
A library needs relocatable objects whose addresses are still placeholders; the linker fills those relocations in later when it knows the final layout (Object files and relocation).
Why does deleting the .a after building leave the executable working?
The linker already copied the required machine code into the executable, so the archive is no longer referenced by the running program — it was a source, not a runtime component.

Edge cases

An empty archive (ar rcs libempty.a with no members) — is it valid?
Yes, it's a well-formed but useless archive; linking -lempty simply resolves nothing and contributes no code, which is harmless unless a symbol was expected from it.
You call a function that isn't defined in any linked object or library — what happens?
The symbol stays undefined through the whole single pass and the linker emits an "undefined reference" error; no archive can rescue a symbol nobody provides.
A used member of the .a itself references a symbol in another member — does that member get pulled too?
Yes — pulling one member adds new undefined symbols to the want-list, so the linker re-scans the same archive's index and pulls the provider member (that's why order within an archive is forgiving, but order across libraries is not).
Two functions live in the same .o inside the library and you call only one — what comes in?
Both — the linker's unit of selection is the member object, so calling any symbol in that .o drags the whole object (and all its code) into the executable.
The same symbol is defined both in your main.o and in a library member — which wins?
Your own object satisfies the reference first, so the library member is simply never pulled for that symbol; the archive is consulted only for symbols still undefined.
Linking the same -lmath twice on the command line — pointless or sometimes useful?
Sometimes useful — a second appearance gives the linker another chance to resolve symbols that became undefined after the first scan, an alternative to --start-group for ordering knots.

Recall The single sentence that explains link-order failures

The linker keeps only symbols that are undefined so far, so a provider listed before its user is scanned with an empty want-list and contributes nothing.