5.3.1 · D5Build Systems & Toolchain

Question bank — Compilation stages — preprocessing, compilation, assembly, linking

1,583 words7 min readBack to topic
Figure — Compilation stages — preprocessing, compilation, assembly, linking

The pipeline shorthand, now with every extension named: hello.c (your source) → hello.i (preprocessed translation unit) → hello.s (assembly) → hello.o (object file) → hello (executable). The figure above traces this and the symbol flow.


True or false — justify

#include files are compiled independently and then merged.
False. A header is never compiled on its own; the preprocessor pastes its raw text into the source, and only the combined translation unit (the .i) is compiled.
The preprocessor understands C types and operator precedence.
False. It is a blind text-substitution engine; it never evaluates math or types, which is exactly why unparenthesized macros misbehave.
An object file (.o) contains real machine code, so you can execute it directly.
False. It holds machine code but also unresolved symbols and placeholder addresses; only the linker turns it into a runnable file.
undefined reference to 'foo' is a compiler (syntax) error.
False. Each file compiled fine; this is a link-time error meaning no object file or library defines foo.
Every stage produces a file that is closer to what the CPU executes.
True. .c → .i → .s → .o → executable each strips away human abstraction and moves toward raw opcodes.
The compiler needs the full body of printf to compile a file that calls it.
False. It only needs printf's declaration/signature (from the header) to generate a call; the body is supplied later at link time.
Optimizations like constant folding (2*3 → 6) happen in the assembler.
False. They happen in the compiler during code generation; the assembler is a near-mechanical mnemonic-to-bytes translation.
Static and dynamic linking produce identical executables except for size.
False. Static copies library code into the binary (self-contained); dynamic stores only a reference loaded from a .so/.dll at runtime — different behaviour, not just size. See Static vs Dynamic Linking.
Stripping comments is done by the compiler.
False. Comment removal is part of preprocessing, before the language grammar is even considered.
If two .o files each define a global function helper, the linker just picks one.
False. That is a duplicate-symbol / multiple-definition error; a non-inline symbol must be defined exactly once across all object files.

Spot the error

"My syntax is fine but I get undefined reference, so I'll add a semicolon."
Wrong fix — a link error has nothing to do with syntax. You forgot to provide the definition: link the missing .o or add the library (e.g. -lm).
#define SQ(x) x*x then SQ(3+1) giving 7 — "the compiler miscalculated."
The compiler is innocent. The text paste produced 3+1*3+1, and precedence made 1*3 bind first. Fix the macro: #define SQ(x) ((x)*(x)).
"I ran gcc -c main.c and it made no executable — the compiler is broken."
-c deliberately stops before linking, producing only main.o. You must run a separate link step to get an executable.
"Header change didn't take effect, so the compiler cached it."
More likely the build didn't re-preprocess the affected units. Since headers are pasted at preprocess time, files that include the header must be rebuilt — see Make and incremental builds.
"gcc -E hello.c printed thousands of lines — my file got corrupted."
Nothing corrupted. -E stops after preprocessing (the .i stage), and #include <stdio.h> pasted the entire header's text, which is why the output ballooned.
Two source files each call helper; file B fails to build. "The linker rejected file B."
If B fails on its own, it failed at compilation, not linking — and in modern C (C99+) calling helper without a visible declaration is itself a compile error (implicit function declarations are forbidden), so B never reaches the linker.

Why questions

Why does the preprocessor need the inner parentheses in ((x)*(x))?
Because it substitutes text only; parentheses force the intended grouping so surrounding operators can't break the expression. See Preprocessor macros and include guards.
Why is an .o file called relocatable?
Its addresses are placeholders; the linker can relocate (patch) them to final positions once it knows where each piece lands in the executable. See Object files and symbol tables.
Why split compilation into four separate stages at all?
Each stage has one job, so it can be inspected, replaced, or optimized independently — and bugs are isolated to a single, debuggable transformation.
Why can two files compile successfully yet fail to link together?
Compilation only checks each unit against declarations; the linker is the first tool to demand that every referenced symbol actually has a definition somewhere.
Why does the return value 42 end up in a register like eax on x86?
The ABI / calling convention fixes which register carries return values, so callers and callees agree without seeing each other's code — see ABI and calling conventions.
Why does dynamic linking make executables smaller?
The library code isn't copied in; only a reference is stored, and the shared library is loaded at runtime. See Static vs Dynamic Linking.
Why must the compiler run after the preprocessor, never before?
The compiler parses real C grammar, which cannot exist until all # directives are resolved into a clean translation unit (the .i).

Edge cases

A source file with only #define lines and no code — what does the compiler produce?
After preprocessing it becomes an empty translation unit, so the compiler emits an object file with essentially no code and no defined symbols.
You link an .o that references foo but never calls it on any real path — still an undefined reference?
Yes for a normal reference: if the symbol is referenced at all and unresolved, linking fails; dead-code elimination that removes the reference first can avoid it, but you can't rely on it.
A header included twice in the same file without include guards — what breaks?
Its contents get pasted twice, causing duplicate definitions (types, etc.). Include guards / #pragma once make the second paste a no-op — see Preprocessor macros and include guards.
An empty main.c containing just int main(){} — how far does the pipeline go?
All four stages run: it preprocesses to itself, compiles, assembles to .o, and links successfully into a runnable (do-nothing) executable.
You define helper but spell its declaration helepr in another file — which stage complains?
The caller compiles against a helepr prototype fine, then the linker fails: it looks for helepr's definition and finds only helper.
A macro that expands to nothing, e.g. #define DEBUG_LOG(x) — what does the compiler see?
The preprocessor deletes each DEBUG_LOG(...) call, so the compiler sees a translation unit as if those lines were never written.

Recall One-line self-test

Map each error to its stage. expected ';' ::: Compile (grammar/syntax stage). undefined reference to 'x' ::: Link (missing definition). stdio.h: No such file or directory ::: Preprocess (the #include paste failed).


Connections