5.3.3 · D2Build Systems & Toolchain

Visual walkthrough — Static libraries — .a - .lib, creation and linking

2,039 words9 min readBack to topic

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).

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

Figure — Static libraries — .a  -  .lib, creation and linking
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.