roadmap
week 4 · day 26

The Compile Pipeline & Linker

preprocess → compile → assemble → link

Hard 32 min 180 xp
After this you can
  • Break 'compiling' into its four real stages and say what each produces
  • Explain what the preprocessor does before any real compilation happens
  • Describe object files, symbols, and how the linker resolves references between them
  • Run each stage by hand and inspect the intermediate outputs

"Compiling" is four steps, not one

gcc hello.c -o hello looks like one action, but it's a pipeline of four distinct stages, each with its own input and output. Most bugs and mysteries ("undefined reference," "multiple definition," weird macro behaviour) live at the boundaries between these stages. Take the black box apart and they stop being mysterious. Here's the whole pipeline:

the four stages
  hello.c
     │  (1) PREPROCESS   cpp: expand #include, #define, #if
     ▼
  hello.i   (pure C, no directives)
     │  (2) COMPILE      cc1: C -> assembly
     ▼
  hello.s   (assembly text, the stuff you've been reading)
     │  (3) ASSEMBLE     as: assembly -> machine code
     ▼
  hello.o   (object file: machine code + a symbol table, not yet runnable)
     │  (4) LINK         ld: combine .o files + libraries -> executable
     ▼
  hello     (an executable the OS can load and run)

(1) The preprocessor: text surgery before compiling

Before the compiler sees any real C, the preprocessor does pure text manipulation: it pastes in the contents of every #include file, replaces every #define macro, and includes/excludes code by #if/#ifdef. It doesn't understand C — it moves text. That's why a macro like #define SQ(x) x*x bites you: SQ(1+2) expands to 1+2*1+2 = 5, not 9, because it's blind text substitution. Run gcc -E to see the fully-expanded source and demystify any macro.

(2) Compile and (3) assemble

The compiler proper turns that expanded C into assembly (gcc -S stops here — this is the .s file you've been reading all along in Compiler Explorer). The assembler then turns assembly into machine code packaged in an object file (.o, via gcc -c). An object file has real machine code but is *not runnable yet*: it has holes. If your code calls printf, the object file just records "I need something called printf" — a symbol reference it can't resolve on its own.

(4) The linker: connecting the references

The linker is the stage people understand least, and it's simple in concept. Each object file has a symbol table: symbols it *defines* (functions/variables it provides) and symbols it *needs* (references to things elsewhere). The linker's job is to match every reference to a definition — across all your .o files and the libraries you include — patch the addresses, and produce one runnable executable.

Key idea
Two of the most common errors are just the linker's two failure modes. "undefined reference to X" = some object needs symbol X but no object or library *defines* it (you forgot to link a file or a library). "multiple definition of X" = two objects both *define* X (you put a non-inline function in a header, or defined a global in two places). Once you picture the linker matching needs to definitions, both messages tell you exactly what to fix.
run every stage by handsh
1gcc -E hello.c -o hello.i # (1) preprocess: see the expanded source
2gcc -S hello.i -o hello.s # (2) compile: C -> assembly
3gcc -c hello.s -o hello.o # (3) assemble: -> object file (machine code)
4gcc hello.o -o hello # (4) link: -> runnable executable
5nm hello.o # inspect the object's symbol table
6# 'U printf' = undefined (needed); 'T main' = defined here (text section)

Running the stages separately makes the pipeline concrete: you can open hello.i and see your tiny file ballooned by the expanded headers, read hello.s as plain assembly, and use nm to see that hello.o *needs* printf (marked U) and *defines* main (marked T). The linker is what turns that U printf into a real address by pulling in the C library.

Predict first
You compile two files: a.c defines int helper(void){...} and main.c calls helper(). You run gcc main.c -o prog (forgetting a.c). What error, from which stage, and why?
Note
Libraries come in two flavours the linker handles differently: static (.a, copied into your executable at link time) and dynamic/shared (.so, resolved at load time by the dynamic linker, so many programs share one copy). ldd ./prog lists the shared libraries a program depends on. This split is why some binaries are self-contained and huge, and others are small but need their .sos present to run.
finished reading?
Your task, you write the code

Walk the pipeline

Take a small C program that calls a function defined in a SEPARATE file (util.c defines it, main.c uses it). (1) Run all four stages by hand on main.c (-E, -S, -c) and look at each output: find something the preprocessor pasted in, read the assembly, and use `nm main.o` to see the defined and undefined (U) symbols. (2) Try to link main.o alone and read the 'undefined reference' error; then compile+link util.c too and watch it resolve. Uses only this lesson.

deliverable: main.c + util.c (+ notes on each stage's output and the nm symbols)
build & run
$ gcc -E main.c -o main.i && gcc -S main.i -o main.s && gcc -c main.s -o main.o
$ nm main.o # note 'U' (needed) and 'T' (defined) symbols
$ gcc main.o -o prog # observe the undefined-reference error
$ gcc main.c util.c -o prog # now it links
self-review before running
  • You identified content the preprocessor inserted (from an #include or #define)
  • You read the .s assembly and saw it's what you've been studying
  • nm showed main.o needs a symbol (U) that util.o defines (T)
  • You reproduced 'undefined reference' and fixed it by linking the defining object
stretchBreak a macro on purpose: `#define SQ(x) x*x` then compute `SQ(1+2)` and `100/SQ(2)` and explain the wrong answers from the raw text expansion (view with -E). Then fix it with parentheses and re-check. This is the preprocessor's text-substitution nature made painfully clear.

Self-check

01What are the four stages of building a C program, in order?
02The preprocessor's job is:
03'undefined reference to X' at link time means:
04Why is a .o object file not directly runnable?
0/4 correct · 0/4 checked