Visual walkthrough — Static libraries — .a - .lib, creation and linking
We only use ideas from the parent: an archive `.a` is a box of object files with a symbol index. We will lean on object files and the linker's job as we go.
Step 1 — What a symbol even is
WHAT. Before we can talk about "resolving symbols" we must know what a symbol is.
A symbol is simply a name that machine code hangs on a chunk of itself — the name
of a function (like add) or a global variable. Two flavours exist:
- Defined symbol — "here is the actual machine code for
add, it lives right here." - Undefined symbol — "somewhere I call
add, but I don't have its code; someone must supply it."
WHY this distinction. The whole job of linking is to marry every undefined use to
exactly one defined body. If a marriage fails, you get the dreaded undefined reference.
PICTURE. Below, main.o is a box that needs add (an open socket, undefined,
drawn in amber). add.o is a box that provides add (a filled plug, defined, cyan).

Step 2 — The archive is a box, and it has a table of contents
WHAT. A .a file is a plain container holding several .o members plus a symbol
index — a small table that says which member defines which symbol.
WHY the index exists. Without it the linker would have to crack open and scan every
member to find out who defines add. The index turns that search into a one-line lookup:
add → add.o, sub → sub.o. This is the s in ar rcs.
PICTURE. The box libmath.a holds two member objects. The amber slip taped to the lid
is the index; each entry is an arrow from a symbol name to the member that defines it.

Step 3 — The linker's one and only tool: a "wanted list"
WHAT. We introduce the single mechanism that explains everything: the linker keeps a set of currently-undefined symbols — call it the wanted list. It starts empty.
WHY a set that grows and shrinks. The linker cannot look into the future. It reads your command line left → right, one file at a time. When a file asks for a symbol, that name is added to the wanted list. When a file provides a symbol on the list, the name is crossed off. That is the whole engine.
PICTURE. A notepad titled WANTED. Right now it is empty — we have read nothing yet. The reading head (amber triangle) sits at the far left of the command line, about to move.

Step 4 — Reading a plain object: it can ADD to the wanted list
WHAT. The reading head reaches main.o. This object defines main but references
add. So the linker: pulls main.o in unconditionally (it's a plain object, not an
archive — plain objects are always included), and records that add is now wanted.
WHY plain objects are always taken. You listed main.o yourself; you clearly meant it.
Archives are the only things the linker treats selectively.
PICTURE. The head is now over main.o. Its provided symbol main gets crossed off (it
satisfies the program's entry requirement), and its needed symbol add appears on the
WANTED notepad in amber.

Step 5 — Reading an archive: it is checked AGAINST the wanted list
WHAT. The head now reaches -lmath (the archive libmath.a). Here the rule is
different and this is the crux: the linker asks, "Does any member of this archive define
a symbol that is currently on the wanted list?" For each such member — and only those —
it pulls the member in.
WHY selective. This is the parent's "Selectivity" goal: don't copy sub into your
program if nobody calls sub. The wanted list is the filter.
PICTURE. add is on the notepad. The linker consults the index, finds add → add.o,
and pulls only add.o out of the box. sub.o stays in the box (nobody wanted it,
drawn greyed). Then add is crossed off the list — it is now defined.

Step 6 — Now run the SAME machine with the arguments swapped
WHAT. Give the linker -lmath first, then main.o. Replay the exact same rules.
WHY replay. The machine has not changed at all — only the order of the tape it reads. If the outcome differs, the order alone is responsible. That is the proof.
PICTURE. Time 1: head over -lmath, but the WANTED notepad is still empty (we've
read nothing that asks for anything). Archive rule: overlap of members with the empty list
is empty ⇒ take nothing. add.o stays in the box. Time 2: head reaches main.o, which
now adds add to the wanted list — but the archive is already behind us, and the linker
never looks back. add stays wanted forever ⇒ undefined reference to add.

Step 7 — Edge case: the empty-overlap skip is a feature, not a bug
WHAT. Consider a program that calls add but never sub, linked correctly
(main.o -lmath). sub.o is never pulled in.
WHY show this. It proves the selectivity claim and warns about a real trap: if sub.o
contained a needed side-effect (say, a global constructor that registers something),
static linking would silently drop it because nobody referenced its symbols.
PICTURE. Two members in the box; the wanted list only ever holds add. add.o is
pulled (cyan), sub.o is left behind (grey, dashed). The final executable literally does
not contain sub's bytes.

Step 8 — Edge case: mutual dependency (a cycle)
WHAT. Suppose libA calls a function in libB, and libB calls one back in libA.
No single left-to-right ordering can put each before the other.
WHY it breaks the simple rule. Whichever library you place last, the other one has already been read and passed. Its newly-wanted symbol from the last archive can't reach back.
PICTURE. -lA then -lB: reading A wants b, reading B supplies b and wants
a — but A is behind us, so a stays unresolved. A red loop shows the deadlock. The fix,
-Wl,--start-group -lA -lB -Wl,--end-group, tells the linker to re-read the group until
the wanted list stops changing — drawn as a circular arrow around both boxes.

The one-picture summary
Everything above is one diagram: a tape read left→right, a wanted list that grows at plain objects and shrinks at archives, and the fatal asymmetry that an archive can only give what was already asked for.

Recall Feynman retelling — explain the whole walkthrough to a friend
The linker is a person walking down a line of boxes with a shopping list, and they can
only walk forward, never turn around. Ordinary boxes (.o files) they always take —
and every time they open one, any part it needs gets scribbled onto the shopping list,
while any part it has gets crossed off. A library box (.a) is special: the walker only
takes items from it that are already written on the list right now. So if you set the
library box down before you've written what you need, the walker glances at it, sees
nothing on their list, takes nothing, and strolls past. Two boxes later you finally write
"I need add" — too late, the library is behind them and they won't go back. Put the
needing box first, the library after, and the walker crosses add off the moment they
reach the box. When two libraries need each other, no single line-up works, so you fence
them together and tell the walker to pace back and forth over just that pair until the
list stops changing.
Related: the pipeline that makes the .o files · the run-time cousin · automating the order · what's inside an object.