The four stages: preprocessor, compilation, assembly, linking
Trace what gcc does in four distinct stages — preprocessing, compilation to assembly, assembly to object code, and linking — and understand each stage's inputs and outputs.
- Name and describe the four stages of C compilation
- Invoke gcc to stop after each stage and inspect the output
- Explain what a linker error is and how it differs from a compiler error
- Describe what an object file contains
When you run gcc hello.c -o hello, you are running four programs in sequence: the preprocessor, the compiler proper, the assembler, and the linker. Understanding each stage explains why certain errors happen where they do, what header files really are, and how multi-file projects are built.
Stage 1: Preprocessing
The C preprocessor (cpp) runs before any C code is compiled. It handles:
#include— pastes the contents of the named file.#define— performs textual substitution.#ifdef/#ifndef/#endif— conditional compilation.#pragma— compiler-specific directives.
The output is valid C with all macros expanded and includes resolved. To see it:
gcc -E hello.c -o hello.iOpen hello.i and you will see hundreds of lines of declarations from <stdio.h> before your five lines of code. The preprocessor is a text tool — it knows nothing about C syntax.
# Define a macro at the command line:
gcc -DDEBUG=1 -E hello.c | grep -A3 "ifdef DEBUG"Stage 2: Compilation
The C compiler translates preprocessed C source into assembly language for the target architecture:
gcc -S hello.i -o hello.sOpen hello.s to see x86-64 assembly. The compiler:
- Checks syntax and type correctness.
- Performs optimisations (with
-O1,-O2,-O3). - Generates assembly that calls into the C runtime and standard library.
Compiler errors (syntax, type mismatches, undeclared variables) are caught here.
Stage 3: Assembly
The assembler (as) translates assembly into machine code — object files (.o files):
gcc -c hello.s -o hello.oOr directly from C source:
gcc -c hello.c -o hello.oAn object file contains:
- Machine code for the functions defined in this source file.
- Symbol table: names and addresses of defined symbols (functions, globals).
- Relocation entries: placeholders for addresses of symbols defined elsewhere.
You can inspect an object file:
nm hello.o # list symbols
objdump -d hello.o # disassemble machine codeObject files are the currency of the linker.
Stage 4: Linking
The linker (ld, invoked by gcc) combines object files and libraries into the final executable:
gcc hello.o -o helloThe linker:
- Resolves references: fills in relocation entries where one object references a symbol in another.
- Merges sections from all objects into a single file.
- Links in standard library object files for things like
printf.
Linker errors look like undefined reference to 'functionname'. This means the function was declared (the compiler saw a prototype) but never defined in any of the object files or libraries being linked.
# Typical linker error:
undefined reference to `sqrt'
# Fix: link the math library
gcc program.c -o program -lmAll four stages in one command
You do not need to run them separately — gcc source.c -o output does all four. But knowing the stages lets you:
- Stop at preprocessing to debug macro expansions (
-E). - Stop at compilation to examine generated assembly (
-S). - Stop at assembly to get an object file for inspection (
-c). - Understand why a linker error is different from a compiler error.
Compiler flags that matter
| Flag | Effect |
|---|---|
-Wall -Wextra | Enable most useful warnings |
-g | Include debug information |
-O0 | No optimisation (best for debugging) |
-O2 | Standard optimisation for release |
-std=c11 | Compile as C11 |
-c | Compile to object file, do not link |
-I path | Add directory to include search path |
-L path | Add directory to library search path |
-l name | Link library libname.a or libname.so |
Separate compilation is why headers exist. When you have ten source files, you compile each to a .o independently and then link. The header file contains the declarations needed for separate compilation — function prototypes, struct definitions, macro constants — so each .c file can be compiled without seeing the implementations of functions defined in other .c files.
Where to go next
Next: header files and include guards — how to structure declarations correctly, prevent duplicate inclusions, and understand the include path.
Lab: CSV parser
Parse a comma-separated value file in C using fgets and sscanf — applying file I/O, string processing, and error handling together.
Header files and include guards
Write proper C header files — declarations vs. definitions, include guards, forward declarations, and the include search path.