Exercises — C compilation — preprocessor, compiler, assembler, linker
Before we start, one shared picture of the whole pipeline — every exercise lives somewhere on this line.

Level 1 — Recognition
Goal: name the tool, name the file, no reasoning chains yet.
L1.1 — Match tool to job
For each job, name the single tool responsible: (a) pastes a header file's text in place; (b) turns mov/add mnemonics into raw opcode bytes; (c) resolves printf to its real code in libc; (d) type-checks your C and emits .s.
Recall Solution
(a) Preprocessor (cpp) — text substitution of #include.
(b) Assembler (as) — mnemonics → machine code.
(c) Linker (ld) — matches undefined symbols to definitions.
(d) Compiler (cc1) — parses, type-checks, emits assembly.
What it looks like: point at each numbered arrow in the s01 figure. #include sits on arrow ① (cpp), mnemonics→bytes on arrow ③ (as), symbol resolution on arrow ④ (ld), type-checking on arrow ② (cc1).
L1.2 — File-form order
Put these file extensions in the order they are produced: .o, .i, .s, .c, executable.
Recall Solution
Why this order: each stage lowers the level by exactly one step. You cannot skip: the assembler needs .s, and .s only exists after the compiler ran.
Mnemonic: "I Saw Our executable" — i, s, o, exe.
L1.3 — Flag to stopping point
Name the gcc flag that stops the pipeline right after each stage: preprocessing / compiling / assembling / (no flag).
Recall Solution
gcc -E ::: stop after preprocessing → .i
gcc -S ::: stop after compiling → .s
gcc -c ::: stop after assembling → .o (not runnable)
gcc (no flag) ::: run the whole pipeline → executable
Picture: each flag is a "STOP HERE" gate drawn under its file box in the s01 figure.
Level 2 — Application
Goal: run flags on concrete input and predict exact output.
L2.1 — Predict preprocessor output
Given:
#define PI 3.14
#define AREA(r) (PI*(r)*(r))
// a circle
double a = AREA(2);Write the exact source after gcc -E (ignore the pasted stdio boilerplate; just the shown lines).
Recall Solution
The comment is stripped, and PI and AREA(r) are substituted as text only:
double a = (3.14*(2)*(2));That is the entire output of gcc -E: a string of characters. The preprocessor does not multiply anything — the file still literally contains (3.14*(2)*(2)), not 12.56. (Only much later, at compile or run time, will something evaluate that expression to .)
Why parentheses matter here: even with argument 2 there is no ambiguity, but the macro author wrapped PI*(r)*(r) defensively so that AREA(1+1) would also work. Text substitution has no idea what a "number" is.
L2.2 — Which flag for which file?
You currently have math.c. You want, in three separate commands: (a) only math.i, (b) only math.s, (c) only math.o. Write the commands.
Recall Solution
gcc -E math.c -o math.i # (a) preprocessed source
gcc -S math.c # (b) produces math.s automatically
gcc -c math.c # (c) produces math.o automaticallyNote: -S and -c name the output file for you (.s, .o); -E prints to stdout unless you redirect with -o.
L2.3 — Count the stages a command runs
How many of the 4 tools does gcc -c file.c actually invoke?
Recall Solution
Three: preprocessor → compiler → assembler. It skips the linker, which is exactly why the result (.o) is not runnable.
Picture: on the s01 figure, -c walks arrows ①②③ and stops before arrow ④.
Level 3 — Analysis
Goal: read an error / behaviour and deduce which stage produced it and why.
L3.1 — Which tool complained?
Classify each message by the tool that emits it:
(a) expected ';' before 'return'
(b) undefined reference to 'sqrt'
(c) 'printf' undeclared (first use in this function)
(d) multiple definition of 'globalCounter'
Recall Solution
(a) Compiler (cc1) — a syntax error; the parser choked.
(b) Linker (ld) — a definition was never found among your .o files and libraries.
(c) Compiler (cc1) — a declaration is missing (you forgot #include <stdio.h>), caught during type-checking, not linking.
(d) Linker (ld) — two object files each defined the same symbol; the linker cannot choose.
Key distinction: undeclared (compiler: "I don't know this name's signature") vs undefined reference (linker: "I know the signature, but no code exists"). See Object Files and Symbol Tables.
L3.2 — The sqrt mystery
#include <math.h>
int main(){ double r = sqrt(2.0); return (int)r; }This compiles but fails at link with undefined reference to 'sqrt'. Why does including math.h not fix it, and what single flag does?
Recall Solution
math.h supplies only the declaration (the promise "double sqrt(double) exists"). That satisfies the compiler. But the compiled code for sqrt lives in the math library libm, which is not linked by default. You must tell the linker to include it:
gcc prog.c -lmHeader = promise; library = delivery. This is the parent note's rule made concrete. Contrast with printf: its code is in libc, which is linked automatically.
L3.3 — Trace the double increment
#define SQ(x) ((x)*(x))
int i = 5;
int y = SQ(i++);What is y, and what is i afterwards? Explain via the expanded text.
Recall Solution
Substitution gives int y = ((i++)*(i++));. The macro duplicated i++, so i would be incremented twice in one expression with no sequence point between them. That makes this undefined behaviour: the C standard guarantees nothing about the result — the value of y, the final value of i, and even whether the program crashes are all unspecified. Any number you happen to observe (some compilers print 30, others 36, others something else) is incidental, never something you may rely on. The only correct answer is: the behaviour is undefined; do not write this.
The lesson: a macro is text, not a function. A function would evaluate i++ once into a temporary; the macro pastes it verbatim. This is why side-effecting arguments break macros — see C Preprocessor Directives.
Level 4 — Synthesis
Goal: assemble a full multi-file build and reason about incremental rebuilds.
L4.1 — Build a two-file program by hand
You have main.c (calls greet()) and greet.c (defines greet()). Write the commands to (a) produce both object files separately, then (b) link them into ./app.
Recall Solution
gcc -c main.c # -> main.o (greet is UNDEFINED here)
gcc -c greet.c # -> greet.o (greet is DEFINED here)
gcc main.o greet.o -o app # linker resolves greet, makes ./appWhat happens at link: main.o's symbol table lists greet as undefined; greet.o lists it as defined. The linker matches them, performs relocation (assigns final addresses), and staples on the _start entry code that calls main.
L4.2 — Incremental rebuild cost
After building app above, you edit only greet.c. Which of the four stages must re-run, and on which files?
Recall Solution
Only greet.c changed, so:
- Re-run preprocessor + compiler + assembler on
greet.c→ newgreet.o. main.ois untouched and reused.- Re-run the linker on
main.o+ newgreet.o→ newapp.
gcc -c greet.c # rebuild only this object
gcc main.o greet.o -o app # relinkWhy this is fast: we skipped recompiling main.c entirely. This is the whole reason object files exist and the engine behind Makefiles and Incremental Builds.
L4.3 — Where does optimization live?
You compile with gcc -O2 -c hot.c. Which stage does -O2 affect, and does it change the .o's symbol table?
Recall Solution
-O2 affects the compiler stage (cc1) — it rearranges/eliminates instructions to produce faster assembly. See Compiler Optimization Levels (-O0..-O3).
It does not change which symbols are defined/undefined (the interface), so the symbol table's names are the same — the machine code behind them is just faster/smaller. That is why you can optimize one .o without re-linking logic changing.
Level 5 — Mastery
Goal: subtle, whole-picture cases — sign/degenerate/limiting analogues of "all cases".
L5.1 — The static symbol
// util.c
static int helper(){ return 42; }main.c writes int x = helper();. Predict the exact stage and error.
Recall Solution
static at file scope gives the symbol internal linkage — helper is not exported in util.o's symbol table. So main.o has helper as undefined, and no .o provides it publicly.
Result: linker error, undefined reference to 'helper'. The compiler is fine (it trusts the declaration in main.c).
The subtlety: the code exists in util.o, but is deliberately hidden. This is the "definition present but invisible" degenerate case — different from "definition absent." See Object Files and Symbol Tables.
L5.2 — Static vs dynamic delivery
Two builds of the same program: gcc app.c -o app_dyn (default) and gcc app.c -static -o app_stat. In which build does printf's machine code physically sit inside the executable file, and which needs libc present at run time?
Recall Solution
app_stat: the linker copiesprintf's code fromlibc.ainto the executable → it is self-contained, larger, no run-time library needed.app_dyn: the executable stores only a reference ("findprintfinlibc.soat run time"); the loader resolves it when you launch → smaller file, butlibc.somust exist on the machine. This is exactly the Static vs Dynamic Linking trade-off: size + self-containment vs sharing + smaller binaries.
L5.3 — Two mains, one program
You link a.o and b.o, and both define int main(). Which stage fails and why, and how is this the mirror image of an undefined reference?
Recall Solution
Linker fails: multiple definition of 'main'.
Mirror image: an undefined reference = zero definitions found; a multiple definition = more than one found. The linker's job is to match each undefined symbol to exactly one definition. Both zero and two violate "exactly one." These are the two failure poles around the healthy case of one definition.
L5.4 — Predict the whole trace
For gcc SQ.c -o sq where
#define SQ(x) ((x)*(x))
int main(){ return SQ(3+1); }state, stage by stage: (i) the preprocessed return line, (ii) whether it compiles, (iii) whether it links, (iv) the process exit code.
Recall Solution
(i) After cpp: return ((3+1)*(3+1)); — this is still just text; no arithmetic has happened yet.
(ii) Compiles cleanly — valid C. During compilation the constant expression is folded.
(iii) Links cleanly — main defined, no external symbols.
(iv) The folded value is , so the program returns 16; the shell exit code is 16 (fits in the 0–255 range).
Contrast the trap: had the macro been #define SQ(x) x*x, the pasted text would be 3+1*3+1, which C evaluates with * before + as 3 + (1*3) + 1 = 7. The parentheses in the good macro are what force the answer 16, not 7 — precedence is decided by the pasted text, not by the macro's "intent." This is why macro parameters are always wrapped in parentheses.
Wrap-up recall
Recall The one-line diagnostic rule
Which tool spoke ::: syntax/undeclared/type → compiler (cc1); undefined or multiple definition → linker (ld)
Header changed, how many objects rebuild ::: every object whose .c includes that header
static function called cross-file ::: linker undefined reference (code exists but not exported)
-lm needed because ::: math.h is only the declaration; sqrt code lives in libm, not linked by default