C Part I · Module 11 · Building Multi-File C Programs
What the Linker Does and Why It Fails
The linker has appeared four times in this module and never said much. It merged two tentative definitions, rejected a definition that landed in a header, refused to find a function somebody made private, and quietly accepted a type mismatch that no stage could see. Every one of those messages named a symbol and nothing else — no line, no file you wrote, no explanation. That is not unhelpfulness; it is an accurate reflection of how little the linker knows, and it means these errors are diagnosed by a procedure rather than by reading. This chapter establishes the procedure, starting from a measurement that shows why reading cannot be enough: two completely different mistakes that produce character-for-character identical output.
Advanced19 min readCPart IFundamentalsBuild
Part I · Module 11 · Chapter 11.9 · What the Linker Does and Why It Fails
1. The Linker's Entire Job
Chapter 11.8 §3 showed the data it works from. Each object file carries a list of symbols it provides and a list it wants:
main.o: U _add U _call_count T _main U _printf
util.o: T _add S _call_countFor every wanted name, the linker counts the definitions available and requires exactly one. It then writes that definition's address wherever the name was used. That is the whole job.
Three things it does not do, each established by measurement earlier in this module:
It does not know types. Chapter 11.1 §6's double add(double, double) against int add(int, int) linked, because both sides spelled the symbol _add.
It does not know files or lines. Those were discarded two stages earlier (Chapter 11.8 §4). It knows which object file a reference came from, which is coarser and is the best any linker message can offer.
It does not know your intent. A name is a string. Two unrelated functions that happen to share a name are, to the linker, one function defined twice.
2. Why Reading the Message Harder Cannot Work
Two programs. In the first, the file containing the definition is simply left off the command line:
/* u_main.c */ int helper(void);
int main(void) { return helper(); }
/* u_help.c */ int helper(void) { return 7; }$ cc -std=c17 -Wall -Wextra u_main.c -o u1 /* u_help.c omitted */
Undefined symbols for architecture arm64:
"_helper", referenced from:
_main in u_main-b00434.o
ld: symbol(s) not found for architecture arm64In the second, both files are on the command line and the definition's name is misspelled:
/* t_main.c */ int helper(void);
int main(void) { return helper(); }
/* t_help.c */ int helpre(void) { return 7; } /* typo */$ cc -std=c17 -Wall -Wextra t_main.c t_help.c -o u2
Undefined symbols for architecture arm64:
"_helper", referenced from:
_main in t_main-8a5331.o
ld: symbol(s) not found for architecture arm64Character for character the same, apart from the compiler's temporary file name.
These are not two causes with one symptom. From the linker's position they are the same situation: nothing in the link provided
_helper. Whether that is because a file was omitted, a name was misspelled, a function was madestatic, or the definition was never written has no representation in the data the linker holds.
So no amount of careful reading will distinguish them, and the common reflex — stare at the message, then guess — fails about as often as it succeeds. The information is recoverable, but not from the message.
3. The Procedure
Four steps, in order. Each one is cheap and each narrows the answer.
1. Which error is it? Undefined (zero definitions) or duplicate (two or more)? They have disjoint cause lists.
2. What is the exact symbol? Take the name from the message, including the leading underscore if your platform adds one.
3. Ask the objects.
nmon each object file tells you who wants it and who provides it — §5.4. Match the answer to a cause, from §4 or §6, and fix the cause rather than the symptom.
Step 4 matters more than it sounds. Adding a stray definition to silence an undefined symbol, or static to silence a duplicate, both work and both are usually wrong — you have changed what the program means in order to stop a message.
4. Undefined Symbol: The Causes
Ordered by how often they occur, with how to confirm each rather than how to guess.
1 — The defining file was not compiled or not linked in. Confirm: nm the object you expect to provide it; if the symbol is there and uppercase, the file is simply missing from the link line.
2 — The name does not match. A typo in the definition, or a different spelling in the declaration. Confirm: compare the exact strings from nm -u (wanted) and nm (provided) rather than from the source.
3 — The function is static. Chapter 11.4 §7 measured exactly this. Confirm: the symbol appears lowercase in the providing object — t _helper rather than T _helper — which means it exists and the linker is forbidden to match it.
4 — It was declared but never defined. A prototype in a header with no corresponding definition anywhere. Confirm: nm finds it in no object at all.
5 — A library is not linked. The declaration came from a header; the definition lives in a library that is not on the command line. Confirm: the symbol is a standard-library name you did not write.
6 — Link order, with static libraries. An archive only supplies members that are already wanted when the linker reaches it, so a library listed before the object that needs it can be skipped entirely. This is Chapter 11.10's subject; know the category exists.
7 — A C++ translation unit calling C. C++ encodes parameter types into the symbol name and C does not, so the two sides want different strings. The fix is extern "C" on the C++ side. Out of scope here, and the first thing to check if a C and C++ codebase will not link.
A platform note so this chapter does not teach a local accident as a rule: on the machine used for these measurements, a program calling sqrt from <math.h> linked with no -lm, because the maths functions live in the system library that is always linked. On most Unix systems -lm is required and its absence is a classic instance of cause 5. Check your own platform rather than this one.
5. Asking the Objects
This is step 3, and it turns §2's ambiguous message into a determinate answer.
Who wants it:
$ nm -u u_main.o | grep helper
_helperWho provides it:
$ nm u_help.o | grep helper
0000000000000000 T _helperT — uppercase. The definition exists, is external, and is perfectly linkable. So the cause is not a typo and not static: the object file was simply not on the link line. Cause 1, confirmed in two commands.
Had the output been t _helper, lowercase, the answer would have been cause 3 — static — and the fix would be to remove the keyword and declare the function in a header rather than to go hunting the build system.
Had no object produced the symbol at all, the answer would have been cause 2 or 4, and comparing the strings from nm against the declaration finds which.
Two
nminvocations distinguish causes that the error message merges. Learn the three readings — uppercase means linkable, lowercase means present but private, absent means it does not exist — and the undefined-symbol error stops being guesswork.
A quick way to search a whole directory of objects:
$ nm *.o | grep helper6. Duplicate Symbol: The Causes
The good news first: this message is complete. It names every object that supplied the symbol.
/* d_a.c */ int shared(void) { return 1; }
/* d_b.c */ int shared(void) { return 2; }$ cc -std=c17 -Wall -Wextra d_a.c d_b.c d_m.c -o d1
duplicate symbol '_shared' in:
.../d_a-6cdefe.o
.../d_b-9574aa.o
ld: 1 duplicate symbolsBoth files are named. And nm confirms it directly:
$ nm d_a.o | grep shared $ nm d_b.o | grep shared
0000000000000000 T _shared 0000000000000000 T _sharedThe causes, with their fixes:
1 — A definition in a header. Chapter 11.3 §3 measured it: the include guard worked perfectly and the link still failed, because a guard protects one translation unit and each including file is a different one. Fix: move the definition to one .c and leave a declaration in the header.
2 — The same name defined in two source files. Two modules each with a helper, both external. Fix: if neither is part of an interface, make both static — Chapter 11.4 §4's default. If one is shared, rename the other.
3 — Two tentative definitions, on a toolchain that rejects them. Chapter 11.1 §5 measured all three behaviours: two files each with int x; linked by the common extension, and failed with -fno-common. Fix: extern int x; in the header, int x = 0; in exactly one .c.
4 — The same object file linked twice. Listed twice on the command line, or present both directly and inside a library. Fix: the build, not the source.
The pattern behind all four is one sentence from Chapter 11.1: a definition may exist exactly once in the whole program. Every duplicate-symbol error is that rule being enforced, and the fix is always to decide which single place the definition belongs in — never to delete one copy at random.
7. The Failure That Is Not a Link Error
Chapter 11.8 §5's callout named a fourth category, and it belongs here because it is the one the linker is responsible for not catching.
/* v1.c */ double add(double, double); int main(void) { ... add(2, 3) ... }
/* v2.c */ int add(int, int) { return a + b; }Both compile. It links. There is no message of any kind, because _add was wanted and _add was supplied — exactly one definition, Figure 1's first row.
A successful link is not a statement that your program is consistent. It is a statement that every name was supplied exactly once. Types, argument counts, and whether the two sides meant the same thing are not part of the check and were discarded before the linker ran.
The only mechanism that catches this is putting the declaration in a header included by both files, so the compiler sees the declaration and the definition in one translation unit — Chapter 11.3's discipline, Chapter 11.2 §5's boundary, and Chapter 11.4 §7's measured pair of error messages at two different distances. The linker cannot be improved into catching it; it does not have the information.
8. What You Should Be Able to Say Now
- The linker counts definitions of each wanted name and requires exactly one, then writes its address into every use.
- It knows no types, no lines, no intent — only names and object files.
- Three outcomes: one definition resolves; zero is undefined; two or more is duplicate.
- Measured: two different causes produced byte-for-byte identical undefined-symbol output — a missing file and a misspelled definition.
- So reading the message harder cannot work; the information is not in it.
- Undefined names only the wanter; duplicate names every provider. One message is half a sentence, the other is complete.
- The procedure: which error · the exact symbol · ask the objects with
nm· match a cause. nmreadings: uppercase = linkable · lowercase = present butstatic· absent = does not exist.- Measured:
nm -u u_main.owanted_helper,nm u_help.oshowedT _helper— so the file was missing from the link, not the definition. - Undefined causes: file not linked · name mismatch ·
static· never defined · missing library · archive order (11.10) · C++ mangling. - Duplicate causes: definition in a header · same name in two files · two tentative definitions · the same object linked twice — all instances of one definition per program.
- A successful link does not mean the program is consistent — the type mismatch links silently, and only a shared header catches it.
9. Exercises
Exercise 1 — classify the errors. For each, say whether it is undefined or duplicate, what the message does and does not tell you, and your first nm command.
Undefined symbols: "_parse_line", referenced from: _main in app.o
duplicate symbol '_buffer' in: a.o, b.o
Undefined symbols: "_sqrt", referenced from: _compute in math_util.o
duplicate symbol '_init' in: lib.o, lib.oExercise 2 — read the nm output. For each, give the cause and the fix.
nm -u app.o | grep parse -> _parse_line
nm util.o | grep parse -> (nothing)nm -u app.o | grep parse -> _parse_line
nm util.o | grep parse -> t _parse_linenm -u app.o | grep parse -> _parse_line
nm util.o | grep parse -> T _parse_lineExercise 3 — explain the identity. In three sentences, explain why a missing source file and a misspelled definition give identical errors, using §1's description of what the linker holds.
Exercise 4 — resist the stub. A colleague fixes an undefined symbol by adding an empty function with that name. Say what the program now does, why it is worse than the build failure, and what they should have run first.
Exercise 5 — the dangerous fix. A duplicate symbol on a shared counter is fixed by adding static to one definition. Describe the resulting bug, name the chapter that measured it, and give the correct fix as a header/source pair.
Exercise 6 — trace a cause. A project links on your machine and fails with undefined symbols on a colleague's, with no source difference. Give three causes from §4 that fit, and one command each to test them.
Exercise 7 — the silent success. Write two short files that link cleanly and are inconsistent. Say which stage could have caught it, why it did not, and the one change that makes it a compile error.
Exercise 8 — write the checklist. Produce a six-line diagnostic checklist you would pin beside your desk for link errors, with the nm commands spelled out.
10. Summary
- The linker counts definitions and requires exactly one, then substitutes an address.
- It knows names and object files — not types, lines, or intent.
- Three outcomes: resolved · undefined (zero) · duplicate (two or more).
- Measured: a missing file and a misspelled definition gave identical messages. Reading harder cannot work.
- Undefined names only the wanter; duplicate names every provider.
- Procedure: which error · the symbol ·
nmthe objects · match a cause. nm: uppercase = linkable · lowercase =static· absent = nonexistent.- Undefined causes: not linked · name mismatch ·
static· never defined · library · archive order · C++ mangling. - Duplicate causes: header definition · two files · two tentative definitions · object linked twice — all one rule.
- Never silence with a stub or a stray
static— both convert a loud failure into a quiet one. - A clean link is not a consistency proof.
Four of this chapter's causes were not problems with your C at all. A file left off the command line, an object listed twice, a library missing, a library in the wrong position — each one is a statement about how the build was invoked, and so far every build in this module has been invoked by typing compiler commands directly. That stops working at about three files, and it has already cost something: Chapter 11.8 §6 pointed out that an object is stale when any header it included changed, and working out that set by hand is both tedious and the kind of thing people get wrong in exactly the way that produces objects which disagree about a structure's layout. The final chapter of this module assembles a real project — several source files, a header each, one command to build it correctly, and a rule that decides what to rebuild — and then adds the one thing that turns a directory of object files into something another project can use.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
