C / GETTING STARTED
How compilation and linking produce an executable
Trace a .c file through preprocessing, compiling, assembling and linking, and tell from an error's wording which stage failed.
What you will learn
- Name what each of the four build stages consumes and produces
- Explain why a declaration is enough to compile but a definition is needed to link
- Tell a compiler error apart from a linker error by its wording
- Build a multi-file program by producing .o files and linking them in a second step
Understanding How compilation and linking produce an executable
A .c file reaches an executable through four stages, and each one hands a different kind of thing to the next. The preprocessor works purely on text: it strips comments, pastes the contents of every #include in place, expands macros, and deletes the branches of #if that did not win. What comes out is a single translation unit, your file plus a few thousand lines of headers, and that is the only thing the compiler proper ever sees. The compiler parses it, checks types and emits assembly, which the assembler turns into an object file holding real machine code.
An object file still cannot run, because it is full of holes. When your code calls printf, the compiler has only the declaration from stdio.h: it knows the call is legal and how to pass the arguments, but not where printf will sit in memory, so it emits a call instruction with a blank target plus a note saying "undefined symbol printf, patch these bytes". The linker is the first stage that sees all the object files and libraries at once, so it is the one that matches every undefined symbol to a definition, lays the pieces out at final addresses, fills in the blanks, and writes an executable whose entry point is libc startup code that eventually calls main.
That split is why C build errors arrive in two flavours. "implicit declaration of function" or an argument type mismatch comes from the compiler, which is judging one translation unit against the declarations it was handed; "undefined reference to X" and "multiple definition of X" come from the linker, which knows nothing about types and only matches names. So the wording tells you where to look: a compiler complaint usually means a missing or wrong declaration, while a linker complaint usually means a source file or library you forgot to include in the build, or a definition that exists twice.
<stdio.h>
STAGE(n, what)
int main(void)
{
STAGE(1, "preprocessor: pasted stdio.h and expanded these macros");
STAGE(2, "compiler: type-checked the calls and emitted machine code");
STAGE(3, "assembler: wrote stages.o, leaving printf as a named hole");
STAGE(4, "linker: filled that hole with printf from libc");
return 0;
}
The compiler works on one file at a time and leaves named holes behind; the linker is the only stage that sees every file and fills those holes in.
Worked examples
Two files, one executable
Shows that a declaration is all the compiler needs, while only the linker needs the definition.
/* ---------- area.c ---------- */
double rect_area(double w, double h)
{
return w * h;
}
/* ---------- main.c ---------- */
<stdio.h>
double rect_area(double w, double h); /* declaration only */
int main(void)
{
printf("%.1f\n", rect_area(2.5, 4.0));
return 0;
}
/* build: gcc -c area.c main.c then gcc main.o area.o -o area */
Example explained
Line 1The declaration in main.c gives the compiler the parameter and return types, so it can emit the call with the target address left blank.
Line 2gcc -c stops after the assembler: main.o lists rect_area as undefined, area.o lists it as defined.
Line 3gcc main.o area.o -o area is the only command that sees both object files, so that is where the blank address is patched.
Line 4Compiling main.c on its own always succeeds; it is linking main.o on its own that fails, which is how you know the missing piece is a file.
Text the compiler never sees
Demonstrates that #ifdef is settled during preprocessing, one stage before any parsing or code generation.
<stdio.h>
BUILD_DEBUG
int main(void)
{
BUILD_DEBUG
puts("kept: the compiler only ever saw this branch");
puts("deleted: this text never reached the compiler");
printf("60 * 60 = %d\n", 60 * 60);
return 0;
}
Example explained
Line 1#ifdef is resolved by the preprocessor, so the #else line is removed as text and the compiler is never offered a choice.
Line 2Delete the #define line and the other branch is the one that survives; nothing about the compiler's behaviour changed, only the text it received.
Line 360 * 60 is a constant expression, so the compiler works out 3600 while translating and the executable carries the number, not a multiplication.
Line 4stdio.h only declares puts and printf; their machine code is joined to the program by the linker.
Important notes
The linker matches symbols by name and ignores types, so declaring a function differently in two files can compile and link and then misbehave at run time.
With the usual shared libc, the linker only records that printf comes from libc.so; the final address is filled in by the dynamic loader when the program starts.
Common mistakes
Believing #include <stdio.h> copies printf's code into your program; it copies a declaration only, which is why forgetting a library (for example -lm for sqrt) compiles cleanly and then fails at the link stage.
Compiling one file with gcc main.c -o app when the function lives in another file, then hunting for a typo in main.c; "undefined reference" means add the other source or object file to the command.
Putting a function definition (not just its declaration) in a header included by two .c files, which gives the linker the same symbol twice and a "multiple definition" error.
Try it yourself
Change, predict, then run
In one file, declare int cube(int n) above main, call it from main, and put its definition below main, then compile and run it. Now delete the definition but keep the declaration and confirm the error you get is a linker complaint about an undefined reference rather than a compiler complaint.
Open the C workspaceCheck your understanding
gcc -c app.c util.c succeeds for both files, but gcc app.o util.o -o app fails with "undefined reference to `parse_line'". What does that tell you?
- app.c is missing a declaration of parse_line
- No object file handed to the linker contains a definition of parse_line, even though app.c had a declaration for it
- parse_line is declared with the wrong return type in util.c
- The preprocessor failed to expand a macro used inside parse_line
Show answer
Compilation succeeded, so the compiler had a declaration and the call type-checked; the failure came from the stage that matches names to definitions, meaning the body of parse_line is in a file that was never compiled or never passed to the link step. A missing declaration is tempting but cannot be the cause, since that produces a compiler diagnostic about an implicit declaration and the build never reaches the linker.