Static linking and archives (.a)
A static library is not a special binary format with one merged body of code. It is an
archive: a table of relocatable .o members plus an index of symbols those members
define. During a static link, the linker scans objects and archives, pulls in only archive
members that satisfy unresolved symbols it has already seen, then treats those extracted
members like ordinary object files. That extraction rule is why the order of main.o
-lthing vs -lthing main.o can decide whether the link succeeds.
The reset: an archive is searched, not linked whole by default. Object files are always included when named; archive members are included only when they answer a current need.
How it really works
Static linking consumes relocatable objects and produces an executable whose required
library code has been copied into the output. For a plain object file, the rule is simple:
if you name helper.o on the command line, the linker includes it. For an archive such as
libcalc.a, the linker opens the archive index and extracts only members whose definitions
resolve currently undefined external symbols.
The usual Unix naming convention is libNAME.a, used from the driver as -lNAME. The
driver passes the library search path with -Ldir; the linker looks for libNAME.so or
libNAME.a depending on platform defaults and flags. When the static archive is selected,
the symbol index lets the linker jump to the member that defines a needed symbol instead of
reading every object blindly.
The left-to-right rule is the part that bites. If the linker sees main.o first, it records
undefined references such as calc_add and calc_mul. When it later scans libcalc.a, it
extracts add.o and mul.o because those members define the waiting symbols. If it sees
-lcalc before main.o, there are no waiting references yet, so the archive contributes
nothing; after main.o appears, the needed symbols are unresolved and the archive is not
automatically rescanned.
| Input kind | Linker behavior |
|---|---|
main.o |
always included when named |
libcalc.a(add.o) |
extracted only if it resolves a current undefined symbol |
libcalc.a(mul.o) |
same archive-member rule |
libc |
normally added by the compiler driver for hosted C |
Static linking gives you a self-contained copy of the selected library code, but it does not erase ABI rules. The objects must still match the target architecture, object format, calling convention, relocation model, and runtime assumptions. A Linux/ELF archive cannot be linked into a macOS/Mach-O executable just because both contain x86-64 machine code.
Executable artifact: archive extraction and order
The example lives in
examples/toolchain-and-linking/static-linking-and-archives/. It builds two library
objects, packs them into libcalc.a, shows the symbols in the archive, and runs the native
program. The script also checks the non-portable -lcalc main.o order on this host so the
note can be honest about platform behavior.
main.c uses declarations from calc.h:
#include <stdio.h>
#include "calc.h"
int main(void) {
int sum = calc_add(2, 5);
int product = calc_mul(sum, 3);
printf("static archive result: %d\n", product);
return 0;
}
The archive members define the symbols:
#include "calc.h"
int calc_add(int left, int right) {
return left + right;
}
#include "calc.h"
int calc_mul(int left, int right) {
return left * right;
}
Run it:
cd examples/toolchain-and-linking/static-linking-and-archives
./run.sh
Real output from this machine:
== native archive and run ==
__.SYMDEF SORTED
add.o
mul.o
== symbols inside archive members ==
add.o:
0000000000000000 T _calc_add
mul.o:
0000000000000000 T _calc_mul
== library before object on this Apple ld ==
static archive result: 21
== portable order: object before library ==
static archive result: 21
== portability note ==
GNU/ELF linkers commonly require the portable order above for static archives.
This run is on macOS/Mach-O, so C symbols have a leading underscore and Apple ld accepts
this small -lcalc main.o case. Do not generalize that to Linux/ELF. GNU-style archive
search is the stricter rule described above: put objects that create unresolved references
before the archives that satisfy them. On Linux/ELF, the same nm output usually prints
calc_add and calc_mul without the underscore, and the non-portable order commonly fails
with an undefined-reference diagnostic.
Failure modes & trade-offs
- Wrong library order. Static archive extraction is order-sensitive. Put objects that create references before the archives that satisfy them.
- Circular archive dependencies. If
liba.aandlibb.aneed each other, one scan may not be enough. GNU linkers provide grouping options such as--start-group/--end-groupat a cost; another fix is to design cleaner archive boundaries. - Duplicate strong definitions. If two included objects define the same external symbol, the link fails. Archives can hide this until a member is extracted.
- Larger executables. Static linking copies selected library code into each program. That can help deployment, but it can duplicate code across many processes.
- Update friction. A dynamically linked library can be patched in one place; a statically linked program must be relinked and redistributed to pick up library fixes.
- License and ABI constraints. Some libraries impose distribution obligations or expect dynamic linking. Static does not mean "free of policy."
In practice
- Read link lines left to right. When a static link fails, move
-lfooafter the objects that referencefoo, then inspect withnmbefore changing source. - Use
ar -tandnm libx.a. They tell you which members exist and which symbols they actually define. - Prefer explicit object ownership. Headers declare; one
.cowns each definition; archives package objects without changing that rule. - Use static linking deliberately. It is excellent for freestanding runtimes, small deployment envelopes, rescue tools, and OS-project support libraries. It is less pleasant for fast security updates and shared system libraries.
- Let the compiler driver invoke the linker.
gcc main.o -L. -lcalcadds the hosted C runtime pieces; directldmeans you must supply them yourself.
Connects to: Toolchain & Linking · Symbols: definition, reference, and resolution · Object files and what's inside them · Translation units, declarations vs definitions, and linkage
Sources
- GNU binutils —
armanual — archive creation, member listing, and symbol-index behavior for static libraries. https://sourceware.org/binutils/docs/binutils/ar.html - GNU
ldmanual — archive search — how archive members are pulled to resolve undefined symbols and why order matters. https://sourceware.org/binutils/docs/ld/ - Ian Lance Taylor — Linkers series — linker implementation view of archive scanning and symbol resolution. https://www.airs.com/blog/archives/38
- David Drysdale — Beginner's Guide to Linkers — practical explanation of static libraries and the left-to-right link rule. https://www.lurklurk.org/linkers/linkers.html
- John R. Levine — Linkers and Loaders — static linking, libraries, symbol resolution, and loader context. https://linker.iecc.com/
- Bryant & O'Hallaron — Computer Systems: A Programmer's Perspective (CS:APP), ch. 7 — archive files and static linking from the programmer-visible side. https://csapp.cs.cmu.edu/