CThe Compilation Pipeline

The Compilation Pipeline

When you run a single command like gcc file.c -o program, it feels like one step. In reality, GCC (and every C compiler) runs your source code through four distinct stages, each with its own input, output, and dedicated tool. Understanding this pipeline demystifies compiler errors and unlocks flags you'll use for the rest of your C career.

The four stages

Stage

Input

Output

Tool

Preprocessing

.c source file

Expanded source (macros resolved, headers inlined)

The preprocessor (cpp, invoked by gcc -E)

Compilation

Preprocessed C source

Assembly code (.s)

The compiler proper (gcc -S)

Assembly

Assembly code (.s)

Object file (.o)

The assembler (gcc -c, invokes as)

Linking

One or more object files + libraries

Executable

The linker (invoked by gcc, runs ld)

Stage 1 — Preprocessing

The preprocessor runs first and handles every line beginning with #. It textually inserts the contents of #included files, expands #define macros, and resolves #if/#ifdef conditional blocks. No actual C syntax is checked at this stage — it's pure text substitution. You can see the result yourself:

Bash
gcc -E hello.c -o hello.i

Open hello.i and you'll find the full contents of stdio.h (often hundreds of lines) pasted in above your one line of code — this is why compiling even a tiny "Hello, World!" involves processing far more text than you actually typed.

Stage 2 — Compilation

The compiler proper takes the preprocessed C and translates it into assembly language for your target CPU architecture. This is where syntax errors, type mismatches, and most warnings are reported — the compiler is analyzing the meaning of your code for the first time. To stop after this stage and inspect the generated assembly:

Bash
gcc -S hello.c -o hello.s

hello.s is human-readable (if you know assembly) — it shows the exact CPU instructions your C code turned into, which is invaluable for understanding performance or debugging compiler-level issues.

Stage 3 — Assembly

The assembler converts human-readable assembly into machine code — raw bytes the CPU can execute — and packages it as an object file. Object files aren't runnable yet: they may reference functions (like printf) that live in a library and haven't been resolved.

Bash
gcc -c hello.c -o hello.o
Stage 4 — Linking

The linker takes one or more object files and combines them with the library code they depend on — for printf, that's the C standard library — into a single executable. Missing-function errors like undefined reference to 'foo' happen at this stage, not during compilation, because the linker is the component responsible for resolving those references.

Bash
gcc hello.o -o hello

Bash
./hello
Hello, World!

Running the full pipeline in one command — gcc hello.c -o hello — simply chains all four steps together automatically, using temporary files for the intermediate results instead of the named .i, .s, and .o files shown above.

Why this matters in practice
Recognizing which stage an error comes from tells you where to look. A macro not expanding as expected is a preprocessing issue; a syntax error is a compilation issue; an "undefined reference" is a linking issue. Learning to tell these apart saves a lot of confused debugging time.