This page is the exhaustive drill for Static libraries — .a / .lib, creation and linking . The parent taught you what a static library is and the three-stage pipeline. Here we hammer every situation the archiver and linker can throw at you — every link-order case, every degenerate archive, every "why did it break" moment — until nothing surprises you.
If a term below feels unfamiliar, it was built in the parent note or in The Linker — symbol resolution & relocation and Object files and relocation .
Think of building-and-linking-a-static-library as a machine with a few dials . Each dial has a few settings. A "scenario" is one combination of settings. The matrix below lists every dial and its extreme settings — the examples afterward each land on one row so that, together, they cover every cell .
#
Dial (what varies)
The settings we must cover
A
Link order
user-before-lib ✓ · lib-before-user ✗ · cyclic A↔B (needs a group)
B
Symbol index present?
ar rcs (indexed) · ar rc (no index → error/repair with ranlib)
C
Selectivity
archive has extra members you don't call → they must be omitted from the exe
D
Degenerate archive
empty .a (0 members) · single-member .a
E
Naming
libX.a + -lX ✓ · X.a + -lX ✗ (must pass full path)
F
Runtime dependency
delete the .a after build → exe still runs (the static guarantee)
G
Duplicate symbols
same symbol defined in two members → which one wins?
H
Platform
Unix ar · Windows lib.exe (same three stages)
I
Word problem
"50 functions, 12 objects" — measure the size win of selectivity
J
Exam twist
reorder a broken command line to make it link
The dials are signs and quadrants of the build world : just as an angle can land in any of four quadrants, a link command lands in one of these rows, and the "naive" approach fails in specific ones (A, B, E) exactly the way the naive arctan fails in quadrants II–IV.
Worked example The happy path, and proof it's self-contained
Files: add.c defines add, main.c calls add. Build a lib, link, then delete the archive and run again.
gcc -c add.c -o add.o
ar rcs libmath.a add.o
gcc -c main.c -o main.o
gcc main.o -L. -lmath -o app
./app # prints, say, add(2,3) = 5
rm libmath.a
./app # STILL prints 5
Forecast: Guess before reading — after rm libmath.a, does ./app still work? Why or why not?
gcc -c add.c -o add.o — Why this step? We want relocatable machine code with placeholder addresses, no main, so we stop before linking. (See Object files and relocation .)
ar rcs libmath.a add.o — Why this step? r inserts the member, c creates quietly, s writes the symbol index so the linker can find add without scanning.
gcc main.o -L. -lmath -o app — Why this step? main.o comes first so add is on the linker's "wanted" list before it opens libmath.a.
rm libmath.a; ./app — Why this step? To prove the archive was only a build-time source.
Verify: add(2,3) should give 2 + 3 = 5 . The exe still prints 5 after deleting the archive because at link time the linker copied add's machine code into app. The .a was a box of LEGO — the brick is already glued on.
Worked example The classic broken order — then fix it
gcc -L. -lmath main.o -o app # ✗ undefined reference to `add`
Exam twist: Reorder the same tokens so it links. Do not add or remove anything.
Forecast: Which token must move, and where?
Trace the linker left→right. At -lmath, its set of undefined symbols is empty . Why this matters? The linker pulls a member out of an archive only to satisfy a currently-undefined symbol — so it takes nothing .
Then it reaches main.o, which references add. Why this fails? add is now undefined, but the archive is already behind us; single-pass GNU ld never re-opens it.
Fix: move main.o before -lmath:
gcc main.o -L. -lmath -o app # ✓
Why this works? When the linker reaches -lmath, add is already on the "wanted" list, so it grabs add.o.
Verify: Rule check — a library must appear after every object that uses its symbols. main.o uses add; libmath.a provides add; so main.o must be left of -lmath. ✓ "Users left, libs right."
Worked example Two libraries that need each other
libA.a calls a symbol in libB.a, and libB.a calls a symbol back in libA.a. No single left→right order satisfies both.
gcc main.o -L. -lA -lB -o app # ✗ if B later needs an A symbol
gcc main.o -L. -lB -lA -o app # ✗ symmetric failure
Forecast: Can any linear order work? If not, what construct saves you?
Suppose -lA -lB. A pulls in a member that references bfunc. Why unresolved? bfunc becomes undefined after we passed... actually it's still ahead in -lB, fine. But if that B member references afunc2, we've now left -lA behind → afunc2 unresolved.
Why no linear order fixes it? A cycle means each side references the other; single-pass scanning can only look forward, so one back-reference is always stranded.
Fix — a group tells the linker to re-scan until stable:
gcc main.o -L. -Wl,--start-group -lA -lB -Wl,--end-group -o app
Why this step? --start-group ... --end-group makes ld loop over the enclosed archives repeatedly until no new symbols get resolved.
Verify: After the group closes, the undefined set is empty → link succeeds. The group is the linker's "do it twice (or more)" escape hatch for cycles. Contrast: Shared libraries — .so / .dll resolve at run time and sidestep this ordering entirely.
ar rc with no s
ar rc libmath.a add.o # note: no 's'
gcc main.o -L. -lmath -o app # may error: "archive has no index"
Forecast: The file libmath.a exists and lists add.o under ar t. Why might linking still fail?
ar rc inserts the member but skips the symbol index . Why it feels complete? ar t libmath.a shows add.o; the file is there.
Why it fails? Without the index the linker has no fast symbol → member table. Modern ld sometimes falls back to scanning, but classically it errors with "archive has no index; run ranlib" .
Fix (two options):
ranlib libmath.a # add the index after the fact
# or just rebuild with s:
ar rcs libmath.a add.o
Why this step? ranlib writes exactly the index s would have written — same table add → add.o.
Verify: nm libmath.a should now show a defined symbol line T add. T = defined in the text (code) section; if it were needed-but-absent it would show U. Index present ⇒ link succeeds.
Worked example 12 objects in the archive, program uses 3
A vendor ships libutil.a built from 12 object files (say each ~4 KB of code). Your main.o calls symbols living in only 3 of them.
Forecast: How much code lands in your executable — all 12 objects' worth (~48 KB) or just 3 (~12 KB)?
Linker builds its undefined set from main.o — it lists exactly the 3 symbols you call. Why? Selectivity: an archive is pulled from member-by-member, only for pending undefined symbols.
It opens libutil.a, extracts the 3 members that define those symbols. The other 9 satisfy nothing. Why skipped? No undefined symbol points at them.
Suppose those 3 members are `4 \text{ KB}$ each and pull in no further symbols.
Verify (the arithmetic):
copied = 3 × 4 KB = 12 KB , skipped = ( 12 − 3 ) × 4 = 36 KB .
Fraction of the archive you actually link:
12 3 = 4 1 = 25%.
Only 12 KB, not 48 KB — that's the selectivity dividend the parent note promised. (Caveat: granularity is per object file , not per function — so put one function per .o if you want maximal trimming.)
Worked example The empty and the single-member library
ar rcs libempty.a # zero members
ar t libempty.a # prints nothing
gcc main.o -L. -lempty -o app # ??? if main.o needs nothing from it
ar rcs libone.a add.o # exactly one member
Forecast: Does linking against an empty archive error out? Does a single-member archive behave any differently from listing the .o directly?
Empty archive: it contains 0 members and an index with 0 entries. Why no error? The linker pulls members only to resolve undefined symbols; there are no members, so it pulls nothing. If main.o needed a symbol from this archive it'd error — but for an unrelated symbol the empty archive is simply a no-op.
Single member: libone.a behaves like handing add.o on the command line, except the archive is still conditional : if nothing needs add, add.o is omitted , whereas a bare .o on the link line is always included.
Verify: Count reasoning. Empty archive: members = 0 , so extracted ≤ 0 = 0 . Single-member archive: extracted ∈ { 0 , 1 } — it's 1 only when main.o references add, else 0 . This "archive members are conditional, bare objects are unconditional" distinction is the thing most people miss.
Worked example Wrong filename, and two definitions of the same symbol
Part 1 — naming:
ar rcs math.a add.o # missing the 'lib' prefix
gcc main.o -L. -lmath -o app # ✗ cannot find -lmath
Forecast: Why can't -lmath find math.a?
-lNAME expands to the filename libNAME.a. Why? That's the fixed search rule. -lmath looks for libmath.a, never math.a.
Fixes:
ar rcs libmath.a add.o # rename properly, OR
gcc main.o ./math.a -o app # pass the full path instead of -l
Part 2 — duplicate symbols (Cell G): two members, fast.o and slow.o, both define add, both are in libmath.a.
The linker takes the first member (in archive/index order) that resolves the undefined add, then add is no longer undefined, so the second definition is never pulled . Why no "multiple definition" error? Because the second one is simply not extracted — archives resolve, they don't force-include. (Two bare .o files both defining add on the command line would error.)
Verify: With the first defining member listed first, the exe links once, using exactly one definition. Extracted-definitions-of-add = 1 , not 2 .
Worked example The same three stages, different tool names
cl /c add.c :: - > add.obj (compile only)
lib /OUT:math.lib add.obj :: archive; lib.exe is ar's cousin
cl main.c math.lib /Fe:app.exe :: link
app.exe
Forecast: Which Unix tool does each Windows command correspond to?
cl /c ↔ gcc -c — Why? Both stop at a relocatable object (.obj / .o).
lib /OUT:math.lib ↔ ar rcs libmath.a — Why? lib.exe bundles objects and writes the symbol table in one step (no separate ranlib needed).
cl main.c math.lib ↔ gcc main.o -L. -lmath — Why? You pass the .lib explicitly (MSVC has no strict -l left→right ordering rule; its linker does multiple passes over inputs).
Verify: Stage count = 3 on both platforms (Compile, Archive, Link). The mapping is one-to-one; only spelling differs. See ELF and PE file formats for why the containers (.o/ELF vs .obj/PE) differ underneath.
Recall Which cells make the
naive "libs first" command fail?
Cell A (link order) and, indirectly, cyclic A. Also Cell E fails at file-lookup time. These are the "wrong quadrant" cases.
Recall Why is an empty archive not an error?
Members are pulled only to resolve undefined symbols; zero members ⇒ nothing to pull ⇒ no-op (unless something actually needed a symbol from it).
Recall Bare
.o on the link line vs .o inside a .a — the key difference?
A bare object is always included; an archive member is conditional — included only if it resolves a pending undefined symbol.
Recall Two members define the same symbol — error?
No. The linker extracts the first resolver, then the symbol is defined, so the second is never pulled. (Two bare objects would clash.)
Mnemonic The matrix in one breath
O rder (users left), I ndex (s or ranlib), N aming (libX.a), C onditional members, C opy-at-build (delete-safe). — "OINCC : order-index-name-conditional-copy."
See also: The Linker — symbol resolution & relocation · Make and build dependency graphs · Compilation pipeline — preprocess, compile, assemble, link · Shared libraries — .so / .dll .