How C Programs Are Built
"Compiling" a C program is really four steps: the preprocessor expands #include and #define, the compiler translates C into assembly, the assembler produces machine code, and the linker joins it with libraries into an executable. Knowing the steps makes error messages — and multi-file projects later — far less mysterious.
The four stages
main.c ──▶ preprocessor ──▶ compiler ──▶ assembler ──▶ main.o ──▶ linker ──▶ main (executable)
(#include, (C → assembly) (assembly → (+ C library,
#define) machine code) other .o files)
| Stage | Input → output | See it with |
|---|---|---|
| Preprocess | .c → expanded C source | gcc -E main.c |
| Compile | C → assembly (.s) | gcc -S main.c |
| Assemble | assembly → object file (.o) | gcc -c main.c |
| Link | object files + libraries → executable | gcc main.o -o main |
Normally one command runs all four. You can stop after any stage to look at the intermediate result.
The preprocessor
The preprocessor is a text-substitution step. #include <stdio.h> is literally replaced by the contents of the stdio.h header file, and #define names are replaced by their values:
#include <stdio.h> #define GREETING "Hi there" #define REPEAT 3 int main(void) { for (int i = 0; i < REPEAT; i++) { printf("%s\n", GREETING); } return 0; }
Hi there Hi there Hi there
After preprocessing, the compiler never sees GREETING or REPEAT, only "Hi there" and 3. Lesson 51 covers the preprocessor in depth.
Headers and libraries
A header (.h file) contains declarations: it tells the compiler that a function such as printf exists and what arguments it takes. The function's actual machine code lives in a library — for printf, the C standard library, which the linker adds automatically. A few libraries need asking for, like the maths library on Linux (-lm).
That split explains two different kinds of error:
- Compiler errors — your C is invalid, or you used a function without its header.
- Linker errors ("undefined reference to …", or "undefined symbol" on macOS) — the code compiled, but the linker can't find the function's definition: a missing library or a missing
.cfile.
Errors and warnings
An error stops compilation. A warning means the code is valid C but probably wrong — and in C, "valid but wrong" is the dangerous kind. Here, the comparison uses = (assignment) instead of ==:
#include <stdio.h> int main(void) { int stock = 0; if (stock = 5) { printf("In stock\n"); } return 0; }
main.c:5:15: warning: using the result of an assignment as a condition without parentheses [-Wparentheses]
This program would compile and run — and always print "In stock". Treat every warning as a bug to fix. Many projects add -Werror, which turns warnings into errors.
Compiled vs. interpreted
Python or JavaScript programs are run by an interpreter that reads the source every time. A C program is translated once, ahead of time, into machine code for one kind of processor and operating system. That's why C is fast and needs no runtime installed — and also why you must recompile after every change, and compile separately for each platform.
Try it yourself
Compile this with gcc -E main.c | tail -5 to see the preprocessed end of the file, then with gcc -S main.c and open main.s to see the assembly the compiler generated.
With tax: 60