Code of the Day
AdvancedC compilation model

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.

CAdvanced12 min read
Recommended first
By the end of this lesson you will be able to:
  • 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.i

Open 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.s

Open 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.o

Or directly from C source:

gcc -c hello.c -o hello.o

An 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 code

Object 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 hello

The 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 -lm

All 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

FlagEffect
-Wall -WextraEnable most useful warnings
-gInclude debug information
-O0No optimisation (best for debugging)
-O2Standard optimisation for release
-std=c11Compile as C11
-cCompile to object file, do not link
-I pathAdd directory to include search path
-L pathAdd directory to library search path
-l nameLink 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.

Finished reading? Mark it complete to track your progress.

On this page