Turning main.c into an executable is four separate stages: preprocess → compile → assemble → link. Knowing them makes compiler and linker errors easy to read.
main.c → Preprocessor → main.i → Compiler → main.s → Assembler → main.o → Linker → a.out
The four stages
Preprocessing (
cpp)Text substitution: expands
#include(pastes header files in),#definemacros,#ifconditionals, strips comments. Output: one big translation unit (-E).Compilation (
cc1)Parses the C, type-checks it, optimises it and emits assembly for the target CPU (
-S). Syntax and type errors happen here.Assembly (
as)Converts assembly into machine code in an object file (
.o) with a symbol table — unresolved references to functions defined elsewhere are left as holes (-c).Linking (
ld)Combines object files and libraries (
libc,-lm…), resolves every symbol to an address, and writes the executable. “undefined reference” errors happen here.
$ gcc -E main.c -o main.i # stop after preprocessing
$ gcc -S main.c -o main.s # stop after compiling (assembly)
$ gcc -c main.c -o main.o # stop after assembling (object file)
$ gcc main.o util.o -o app # link object files into an executable
$ gcc -save-temps main.c # keep main.i, main.s, main.o all at once
$ nm main.o # list symbols (U = undefined, T = defined text)Essential gcc / clang flags
| Flag | Meaning |
|---|---|
-o name | Name of the output file (default a.out). |
-std=c17 | Language standard (c89, c99, c11, c17, c2x/c23, or gnu17 with extensions). |
-Wall -Wextra | Turn on the important warnings. Always use them. |
-Werror | Treat warnings as errors (good in CI). |
-pedantic | Warn about non-standard extensions. |
-g | Include debug info (for gdb / lldb). |
-O0 -O1 -O2 -O3 -Os | Optimisation level (none → aggressive; -Os for size). |
-I dir / -L dir / -lname | Add a header search path / library search path / link libname. |
-D NAME=val | Define a macro from the command line (like #define NAME val). |
-fsanitize=address,undefined | Runtime checks for memory errors and undefined behaviour (ASan / UBSan). |
Warnings are free bug reports
Every sample in these notes is compiled with -Wall -Wextra -Werror. This one is rejected at compile time because the format string does not match the argument:
#include <stdio.h>
int main(void) {
double price = 9.99;
printf("%d\n", price); /* %d expects int, got double */
return 0;
}main.c:5:20: error: format specifies type 'int' but the argument has type 'double' [-Werror,-Wformat]
5 | printf("%d\n", price); /* %d expects int, got double */
| ~~ ^~~~~
| %fCompile-time vs link-time errors
| Error | Stage | Typical cause |
|---|---|---|
expected ';' | Compile | Syntax error |
implicit declaration of function | Compile | Missing #include or prototype |
undefined reference to 'foo' | Link | Function declared but never defined / .o or -l library not linked |
multiple definition of 'x' | Link | Global defined in a header included by two .c files |
undefined reference to 'sqrt' | Link | Forgot -lm (math library) on some Linux toolchains |
What is a translation unit?
What is the difference between a compiler error and a linker error?
Static vs dynamic linking?