First question: which stage is talking?
You have now walked the whole pipeline: the preprocessor pastes text, the compiler turns one translation unit into assembly, the assembler makes an object file, and the linker joins the pieces. The single most useful skill when something breaks is to ask, before reading a word of the message: which of those four stages produced this? Because the four stages know completely different things, their errors mean completely different things — and beginners waste hours fixing a function's body when the real complaint came from a stage that never even looked at the body.
There is a fast tell. Compiler messages almost always carry a file name and a line number — `main.c:8:5: error: ...` — because the compiler is reading your text and can point at exactly where it choked. Linker messages do not: they name a symbol, like `undefined reference to 'sqrt'`, with no line number, often prefixed with the linker's own name (`ld:`) or labelled a 'collect2' error. That single difference — line number versus bare symbol name — usually tells you which half of the toolchain you are fighting before you understand anything else about the message.
Compiler errors: a sentence the compiler could not parse
A compiler error means: while reading one translation unit, the compiler hit text that breaks the rules of C — bad grammar, an undeclared name, a type mismatch. The classic shape is `file:line:column: error: message`. Read it strictly left to right. The file and line place you; the column points within that line; the word `error:` marks severity; and the message, in plain English, says what rule was broken. A huge fraction of real errors are mundane: a missing semicolon (which the compiler usually reports on the line after the real one, because that is where the sentence finally stopped making sense), a stray brace, or a typo'd variable name.
One compiler error deserves special attention because it foreshadows the linker. If you call a function the compiler has never heard of, modern C reports `error: implicit declaration of function 'foo'`. This is the compiler telling you it found no prototype — no promise that `foo()` exists and takes these arguments. The cure is almost always a missing `#include` of the header that declares it. Notice the layering: this is a compiler error about a declaration. Even after you fix it, you may still hit a linker error later if the function is declared but never actually supplied — two different stages, two different problems, easily confused.
The famous one: undefined reference
Now the other half. The message `undefined reference to 'sqrt'` is a linker error, and it has a precise meaning: every translation unit compiled fine on its own, but when the linker tried to join the object files, it found a hole — a name some object file needs — that no object file or library provides. In the vocabulary of the previous guide, the symbol is an undefined symbol that stayed undefined. The compiler was happy because a header promised `sqrt()` existed; the linker is unhappy because, at link time, nobody actually delivered the machine code.
There are only a few real causes, and naming them is half the cure. (1) You forgot to link a library: `sqrt()` lives in the math library, so the fix is `gcc main.c -lm` — the `-lm` tells the linker to also search libm. (2) You wrote a function in another `.c` file but forgot to compile and link it in: `gcc main.c helper.c`. (3) A typo or a mismatch between the declaration and the definition — `sqrt` declared but `Sqrt` defined — so the names never match. (4) Order: with static libraries, a library that supplies a symbol must come after the object that needs it on the command line, an old quirk of how the linker scans left to right.
main.c: the verdict: #include <math.h> <math.h> declares sqrt -> compiler: OK double r = sqrt(2.0); sqrt's code is in libm -> linker: ? $ gcc main.c /usr/bin/ld: undefined reference to `sqrt' $ gcc main.c -lm (links libm) -> a.out OK
Its mirror image: multiple definition
If `undefined reference` is 'a needed name with no supplier,' its mirror image is `multiple definition of 'count'` — 'a name with too many suppliers.' This is also a linker error, and it means two object files each provided a real definition of the same global symbol, and the linker refuses to guess which one you meant. The usual culprit is putting a definition in a header. Because the preprocessor pastes a header into every source file that includes it, a line like `int count = 0;` sitting in a header becomes a separate global named `count` in each translation unit — and at link time those collide.
The fix encodes the whole header-versus-source discipline. A header should only declare — make the promise — using `extern int count;`, which says 'a global `count` exists somewhere.' Exactly one source file then defines it once, `int count = 0;`, which actually reserves the storage. Declaration in the header, definition in one `.c`: that single rule is what keeps the linker seeing exactly one supplier per name. (Note: an include guard does not fix this — guards stop a header being pasted twice into one file, but `multiple definition` is across different files, which guards never touch.)
A method for any message you have never seen
You will meet messages this guide never listed. The point was never to memorize a catalogue — it was to give you a procedure that turns any unfamiliar wall of text into a located, named problem. Run it in order, top to bottom, and stop as soon as the fix is obvious.
- Scroll to the FIRST error in the output, ignoring the cascade beneath it for now.
- Classify the stage: does it carry a file:line (compiler), or name a bare symbol with no line (linker)? That decides everything that follows.
- If COMPILER: go to that exact file and line, read the message literally, and check the line above too — a missing semicolon or brace is usually reported one line late.
- If LINKER: read the symbol name. 'undefined reference' means a missing supplier (link the library with -l, add the missing .c, or fix a name typo); 'multiple definition' means a definition leaked into a header.
- Apply ONE fix, rebuild, and re-read from the top — many follow-on errors will already be gone.
- Still stuck on the words? Search the exact message text verbatim; with the stage already identified, you will read the answer with real understanding instead of guessing.
That is the quiet payoff of this whole rung. A red wall of build output used to feel like the machine rejecting you for no reason. Now you know there is no 'the machine' — there are four programs, each with a narrow job, each complaining in its own dialect about its own concern. Name the stage, and the message stops being noise and becomes a precise instruction. You have gone from 'it does not compile' to 'the linker cannot find sqrt because I have not passed -lm' — and that sentence already contains its own cure.