5.3.1 · D4Build Systems & Toolchain

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

2,341 words11 min readBack to topic
Figure — Compilation stages — preprocessing, compilation, assembly, linking

Level 1 — Recognition

(Goal: given a symptom, name the stage that owns it. No coding — just point.)

L1·Q1

For each error, name the stage that produced it: (a) expected ';' before 'return' · (b) undefined reference to 'sqrt' · (c) stdio.h: No such file or directory.

Recall Solution

(a) Compilation. A missing semicolon is a syntax problem — the parser inside the compiler catches it while building the syntax tree. (b) Linking. The symbol sqrt was used but never defined in any object file or library given to the linker. The compiler was happy because it only needed sqrt's declaration. (c) Preprocessing. #include is a preprocessor directive; it failed while trying to paste the header text. The compiler never even ran.

L1·Q2

Match each output file to the stage that creates it: .i, .s, .o, hello (executable).

Recall Solution
  • .iPreprocessing (pure C, no # lines).
  • .sCompilation (human-readable assembly).
  • .oAssembly (raw machine code, still relocatable).
  • helloLinking (symbols resolved, runnable).

L1·Q3

True or false: "A .o file can be executed directly."

Recall Solution

False. An object file holds machine code but still contains unresolved symbols and placeholder addresses (relocation entries). Only after linking do those placeholders become real addresses, making a runnable executable.


Level 2 — Application

(Goal: drive the pipeline yourself with the right flags.)

L2·Q1

You want to inspect the macro-expanded source of prog.c without compiling it. Write the command and name the output extension.

Recall Solution
gcc -E prog.c -o prog.i

-E means "stop after preprocessing." Output is a .i translation unit: all #include files pasted in, all macros expanded, comments stripped, no # lines left.

L2·Q2

Given the macro below, what does the preprocessor produce for line 2, and what integer does it evaluate to?

#define DBL(x) (x)+(x)
int z = DBL(5) * 2;
Recall Solution

Pure text paste of DBL(5)(5)+(5), so line 2 becomes:

int z = (5)+(5) * 2;

Now normal C precedence applies: * binds tighter than +, so this is , not the intended . The bug is a missing outer parenthesis on the whole macro body: it should be #define DBL(x) ((x)+(x)). The preprocessor does not know you meant "the doubled value times two" — it only pastes text.

L2·Q3

You have main.c and util.c. Write commands that compile+assemble each into a .o separately, then link them into app.

Recall Solution
gcc -c main.c -o main.o   # preprocess + compile + assemble main
gcc -c util.c -o util.o   # same for util
gcc main.o util.o -o app  # link both objects into one executable

-c runs the first three stages but stops before linking, giving relocatable .o files. The final gcc with no -c/-S/-E performs only the link step (it detects the inputs are already objects).


Level 3 — Analysis

(Goal: reason about which stage fails and why, across multiple files.)

L3·Q1

math_helpers.c defines double area(double r). main.c calls area but you compiled and linked with gcc main.c -o app only. What error appears, at which stage, and why did compiling main.c succeed?

Recall Solution

Error: undefined reference to 'area' — a link-time error. main.c compiled fine because it only needed the declaration (signature) of area to type-check the call and generate a "call this symbol" instruction. It emits a symbol-table entry area (undefined). Linking failed because math_helpers.o was never given to the linker, so nothing defines area. Fix: gcc main.c math_helpers.c -o app.

L3·Q2

Two source files both do int counter = 0; at global (file) scope and are linked together. Compiling each alone succeeds. What happens at link time and why?

Recall Solution

Each .o defines the global symbol counter. When the linker merges them it finds two definitions of the same symbol → multiple definition of 'counter'. Also a link-time error. Why compile succeeds separately: the compiler only sees one file at a time; within one file there is exactly one definition. The clash only appears when both symbol tables meet at the linker. Fix: define counter in one .c and declare extern int counter; in the other.

L3·Q3

config.h contains int limit = 100; (a definition, not extern). Two .c files #include "config.h". Trace the failure through the pipeline.

Recall Solution
  • Preprocess: each .c gets int limit = 100; pasted in → each translation unit defines limit.
  • Compile / assemble: each succeeds independently; each .o defines the symbol limit.
  • Link: two definitions of limitmultiple definition of 'limit'. Root cause: putting a definition in a header. Headers should hold declarations (extern int limit;) plus include guards; the single definition lives in one .c.

Level 4 — Synthesis

(Goal: assemble whole builds and reason about linking choices.)

L4·Q1

Project files: main.c, parser.c, parser.h (declarations only, guarded), and you need sin from libm. Write a clean two-step build (objects then link) that succeeds.

Recall Solution
gcc -c main.c   -o main.o     # main.o: needs parse(), needs sin
gcc -c parser.c -o parser.o   # parser.o: defines parse()
gcc main.o parser.o -lm -o app

parser.h is never compiled — it is pasted into main.c and parser.c during preprocessing, and its include guard stops double-pasting. -lm tells the linker to search the math library libm to resolve sin. Order matters on many linkers: put -lm after the objects that use it.

L4·Q2

Your teammate ships libgraph.a (static) and libgraph.so (dynamic), both defining draw(). Using each, what is the size/deployment trade-off, and which produces a self-contained binary?

Recall Solution
  • Static (libgraph.a): the linker copies draw's machine code into your executable. Bigger binary, but self-contained — no external file needed at run time.
  • Dynamic (libgraph.so): only a reference is stored; the real code is loaded from the .so at run time. Smaller binary and shared across programs, but the .so must be present (and findable) when the program runs. The static link gives the self-contained binary. Details in Static vs Dynamic Linking.

L4·Q3

You have 4 source files. You edit only parser.c. Which stages must re-run for parser.c, and why does re-linking still happen, but the other three files are not recompiled?

Recall Solution
  • parser.c changed → re-run preprocess + compile + assemble on it, producing a fresh parser.o.
  • The other three .o files are unchanged, so a smart build tool (Make) skips them — this is incremental build.
  • Linking must still re-run, because the final executable depends on the new parser.o; the linker must re-merge and re-resolve symbols with the updated object. See Make and incremental builds. Count of files recompiled: 1. Files re-linked: 1 executable from 4 objects.

Level 5 — Mastery

(Goal: edge cases, ordering subtleties, and full-pipeline reasoning.)

L5·Q1

#define SUB(a,b) a-b
int r = SUB(10, 5) * 2;   // (i)
int s = 100 * SUB(6, 1);  // (ii)

Give the expanded text and the value of each. Then give the corrected macro.

Recall Solution

Pure text paste:

  • (i) SUB(10,5)10-5, line: int r = 10-5 * 2; → precedence: (intended ).
  • (ii) SUB(6,1)6-1, line: int s = 100 * 6-1; (intended ). Both wrong for the same reason — missing parentheses. Fix: #define SUB(a,b) ((a)-(b)), giving , . See Preprocessor macros and include guards.

L5·Q2

Linker command gcc -lm main.o -o app fails with undefined reference to 'sin', but gcc main.o -lm -o app succeeds. Same files, same library. Explain the ordering rule.

Recall Solution

Traditional linkers process inputs left to right, keeping only symbols that are currently needed. When -lm comes before main.o, nothing needs sin yet, so the linker takes nothing from libm and moves on. Then main.o requests sin — but libm was already passed, so sin stays undefined. Rule: list libraries after the objects that use them. The user of a symbol must appear before the provider on the command line.

L5·Q3

Full pipeline trace. For hello.c containing #include <stdio.h> and a call to printf, state — for each of the 4 stages — one concrete thing that happens to the printf reference.

Recall Solution
  • Preprocess: #include <stdio.h> pastes in the declaration int printf(const char*, ...); so the compiler will know printf's signature. No code for printf yet.
  • Compile: type-checks the printf(...) call against that declaration; emits assembly with a call printf and marks printf as an external symbol needed later.
  • Assemble: turns call printf into machine code with a placeholder address plus a relocation entry: "patch this once printf's address is known."
  • Link: finds printf's definition inside libc, resolves the symbol, and patches the placeholder with the real address → runnable executable.

L5·Q4

A file compiles with -c cleanly but the executable crashes at run time with no linker error. Was any compilation-pipeline stage at fault? Explain.

Recall Solution

No. All four stages succeeded: preprocessing pasted text, compilation type-checked, assembly produced valid machine code, and linking resolved every symbol (otherwise you'd get undefined reference). A run-time crash (e.g. dereferencing a bad pointer) is a logic/runtime fault, outside the build pipeline entirely. The pipeline guarantees the program is well-formed and linkable, not that it is correct.


Recall Feynman recap of the whole ladder

Recognition = name the room the noise came from. Application = know which switch (flag) stops the machine where. Analysis = two clean files, one clash — realize it happens where their symbol tables meet (the linker). Synthesis = build a real project and pick static vs dynamic wisely. Mastery = the macro-paste gotchas and left-to-right link ordering that even pros trip on.


Connections