5.3.1 · D1Build Systems & Toolchain

Foundations — Compilation stages — preprocessing, compilation, assembly, linking

3,506 words16 min readBack to topic

Why this page exists

The parent note throws around words like translation unit, symbol table, relocation, object file, register, and ABI as if you already own them. Here we build each one from zero, in the order they stack on top of each other. Nothing below assumes you have seen assembly, compilers, or even what a "file" truly is. Each concept is introduced before any later concept uses it.


The four stages, named once and for all

Before any symbol, fix the four stages the parent note describes, so every later "Stage N" reference below is unambiguous. Each stage is a tool that reads one file and writes a more machine-like one:

Keep this list handy: whenever a note below says "Stage 3" it means assembly, and "Stage 4" means linking.


Symbol 0 — What is a "file", "source code", and a "file extension"?

Figure — Compilation stages — preprocessing, compilation, assembly, linking
Alt text / figure description: a horizontal row of five rounded boxes labelled hello.c, hello.i, hello.s, hello.o, hello, each with a small italic subtitle (text, pure C, assembly, bytes, runs!). Four labelled arrows connect them left to right: "preprocess", "compile", "assemble", "link". A caption underneath reads "Same program the whole way — only its 'clothes' change."

Why the topic needs it: the whole chapter is literally "what does each rewrite do, and what extension marks its output". If you do not see the file as the same program in different clothes, the pipeline looks like four unrelated tools.


Symbol 1 — A "token"

Why the topic needs it: the compiler's very first internal step (lexing in the parent note) is "turn text into tokens, dropping whitespace and comments". Also the preprocessor works by replacing tokens with other tokens (that is exactly what #define PI 3.14 does — swap the token PI for the tokens 3.14).


Symbol 2 — A "directive" and the # sign

The picture: imagine your program text with some lines highlighted. The highlighted (#) lines are editing instructions; everything else is the actual program. Stage 1 (preprocessing) obeys the highlighted lines, then deletes them, leaving only program.

Why the topic needs it: the parent's entire Stage 1 (preprocessing) is "handle every # line and remove it". Recognising # as the preprocessor's marker — even when indented or split across lines with \ — is the key to the whole first stage. See Preprocessor macros and include guards for how these directives are used in practice.


Symbol 3 — A "translation unit" (and what .i holds)

The picture: your small .c file swells up because #include <stdio.h> pastes hundreds of lines of stdio.h into it. That swollen result — one big self-contained slab of C — is one translation unit, and that is exactly what a .i file contains.

Figure — Compilation stages — preprocessing, compilation, assembly, linking
Alt text / figure description: on the left, two small boxes — a lavender hello.c (small) and a butter-yellow stdio.h (big). Two coral arrows labelled "pasted" point right into one large mint box titled "translation unit (.i)". A dashed line inside the big box separates an upper region labelled "hundreds of lines from stdio.h" from a lower region labelled "your few lines of hello.c". Caption: "No more '#' lines remain — this slab is what the compiler reads."

Why the topic needs it: it explains the parent's mistake-buster "header files are not compiled separately". Headers get pasted into a translation unit; only the unit (the .i) is compiled. One unit → one object file.


Symbol 4 — The "compiler" and the "assembler" as two distinct tools

Before we look at .s bytes, name the two tools that produce them, because the parent note treats them separately (Stage 2 and Stage 3).

The picture: the compiler is the author who writes the recipe in chef-shorthand (assembly); the assembler is the typesetter who converts that shorthand, symbol by symbol, into the exact byte codes the machine reads. gcc -S stops after the author; gcc -c runs the author and the typesetter.

Why the topic needs it: the parent enumerates Stage 2 (compile → .s) and Stage 3 (assemble → .o) as distinct. Confusing them makes it impossible to explain why .s is readable text but .o is raw bytes.


Symbol 5 — A "register", eax, and what .s holds

The picture: if normal memory (RAM) is a huge warehouse, a register is a coin held in the CPU's hand — instant to use, but you only have a few hands. The .s file is a step-by-step script that shuffles coins between hands and the warehouse.

Why the topic needs it: the parent's assembly example mov eax, 42 means "put the number 42 into the register named eax". The comment "return value goes in eax" is a convention — which brings us to the next symbol.


Symbol 6 — "ABI", "calling convention", and "stack slot"

The picture: two people who never met can still shake hands because both learned the same custom. The ABI is that custom for machine code: "the return value always rides home in eax", so any caller knows where to look.

Why the topic needs it: it explains why the compiler emitted eax and not some other slot, and it is the invisible glue that lets the linker join code compiled at different times. Deeper detail lives in ABI and calling conventions.


Symbol 7 — A "symbol"

The picture: every object file carries a two-column list — one column "names I provide", one column "names I still need". That list is the symbol table.

Figure — Compilation stages — preprocessing, compilation, assembly, linking
Alt text / figure description: a mint box on the left titled hello.o, split into two columns. Under the heading "I PROVIDE" sits a lavender chip labelled main. Under "I NEED" sit two coral chips labelled printf and helper?. On the right, a butter-yellow box titled libc contains a lavender chip printf. A "resolved" arrow connects the needed printf to the printf inside libc. Red text notes: "helper found nowhere -> 'undefined reference'."

Why the topic needs it: the linker's whole job is matching every "I need X" to some "I define X". The famous undefined reference to 'foo' means: some file needed foo, no file defined it. See Object files and symbol tables.


Symbol 8 — An "address" and a "placeholder / relocation"

The picture: a form with Call ______ where the blank will hold printf's real address once the linker discovers it.

Why the topic needs it: this is why a .o file "cannot be run" (parent's Stage 3, assembly). It still contains blanks. Only after the linker fills every blank (relocation) do you get a runnable executable.


Symbol 9 — The "linker", "object file" vs "executable", "static" vs "dynamic"

Why the topic needs it: these are the two flavours of Stage 4 (linking). Knowing what the linker fills in makes the static/dynamic split obvious — it is just "copy now" versus "look up later".


Symbol 10 — What "incremental build" hints at


How these foundations feed the topic

Read the map below top to bottom: an arrow X --> Y means "you need X before Y makes sense". Start at Source text and file extension (top-left) — it feeds Tokens, which feed Directives and the Translation unit. The translation unit plus the ABI (built from the register and stack slot ideas) feed the Compilation stage (Stage 2), whose output the Assembly stage (Stage 3) turns into an object file carrying Symbols. Symbols gather into a Symbol table; that plus Addresses and their Relocation placeholders feed the Linking stage (Stage 4). Linking yields the object-file-vs-executable distinction and the static/dynamic choice. Every path eventually converges on the single node Preprocess compile assemble link pipeline — the parent topic itself, i.e. the full four-stage flow.

Source text and file extension

Tokens

Directives with hash sign

Translation unit

Register such as eax

ABI and calling convention

Stack slot

Compilation stage two

Assembly stage three

Symbol

Symbol table

Address

Relocation placeholder

Linking stage four

Object file vs executable

Static vs dynamic linking

Preprocess compile assemble link pipeline


Equipment checklist

How to use this: cover the right side of each line with your hand. Read the prompt, say your answer out loud, then uncover to check. If you miss any item, re-read that section before moving to the main topic — you are not ready until you clear all fourteen.

Recite the four pipeline stages in order, each with its tool
preprocess (preprocessor) → compile (compiler) → assemble (assembler) → link (linker)
Explain what a "file" is and what it means for a program to "live in" one
a named lump of bytes on disk; the program's characters are saved as that lump under a name like hello.c
Recite the four output extensions in pipeline order
.c.i.s.o → executable
State what a token is AND what is not a token
smallest meaningful text chunk (word, number, bracket); whitespace and comments are dropped, never emitted
Give two real-world subtleties of a # directive
leading whitespace before # is allowed; a \ at line end continues the directive onto the next line
Say what a .i file contains and how it differs from .c
pure C with headers pasted in and all # lines/comments removed — bigger and cleaner than the .c
State how the compiler and the assembler differ
compiler turns C into assembly text (.s) and decides which instructions; assembler encodes those instructions as raw bytes (.o)
Say what a .s file contains and how it differs from .o
human-readable CPU-specific assembly text; the .o is the same meaning as raw bytes
Define a register and name the x86 return register
a tiny fast storage slot inside the CPU; eax carries the return value
Define a "stack slot" and when it is used
a reserved spot in the stack memory region used to pass a value when the CPU's registers run out
State in one line what an ABI decides
where arguments and return values live (register or stack slot) so separately-compiled code can interoperate
Distinguish a defined vs undefined symbol
defined = this file provides its code; undefined = this file uses the name but the code is elsewhere
Explain what a relocation placeholder is and who fills it
a blank address the assembler leaves; the linker fills it in during relocation
Say in one line what the linker does and when it runs
last stage; combines several object files + libraries, resolves symbols and fills placeholder addresses into one executable
Explain why a .o cannot run
it still has undefined symbols and unfilled placeholder addresses
Contrast static vs dynamic linking in one line
static copies the library bytes in; dynamic stores a reference loaded at run time

Connections