Question bank — C compilation — preprocessor, compiler, assembler, linker
True or false — justify
Recall The preprocessor understands C types and scope.
False. The preprocessor is pure text substitution running before the compiler; it has never heard of int, scope, or type-checking — it only swaps tokens and pastes files.
Recall
#include <stdio.h> copies the compiled code of printf into your file.
False. The header pastes in only declarations (the prototype of printf). The compiled machine code of printf lives in the C library and is stapled in later by the linker.
Recall After preprocessing, no
# directives remain in the code.
True. That is the whole job of the preprocessor — every #include, #define, and #ifdef is resolved, and comments are stripped, leaving plain expanded C (.i).
Recall The compiler outputs raw machine code directly.
False. The compiler outputs assembly (.s), a human-readable text of mnemonics like mov and call. Turning those into opcode bytes is the assembler's job.
Recall An object file (
.o) is a runnable program.
False. A .o contains machine code but has unresolved symbols (holes like printf) and no entry-point setup, so the OS cannot launch it. Only the linker's output runs.
Recall A macro and a small function behave identically.
False. A function is called once with its argument evaluated once; a macro textually pastes its argument, so SQUARE(i++) expands to ((i++)*(i++)) and increments twice.
Recall "undefined reference to 'foo'" is a syntax error.
False. It is a linker error: your syntax and types are valid, but no definition of foo was found anywhere. Different stage, different fix — add the missing source or library.
Recall Recompiling one changed
.c file forces every other file to recompile too.
False. Each .c compiles independently to its own .o; only the changed file is rebuilt and everything is re-linked. This separation of concerns is exactly what make exploits.
Recall The linker checks whether your function arguments match their types.
False. Type-matching is the compiler's job, done per file. The linker only matches names (symbols) — it will happily connect a mismatched call if the name lines up, which is why C's separate compilation can hide such bugs.
Spot the error
Recall "I got 'undefined reference to printf', so my syntax must be broken."
The diagnosis is misfiled. Syntax was fine (the compiler passed); the linker couldn't find printf's definition — usually a missing library flag, not a code typo.
Recall
#define SQUARE(x) x*x then SQUARE(3+1) — why is the result 7, not 16?
The macro pastes text with no parentheses, so it becomes 3+1*3+1. Operator precedence multiplies first: 3 + 3 + 1 = 7. Wrap params and body: ((x)*(x)).
Recall "gcc -c gave me hello.o but running ./hello.o does nothing — the compiler is broken."
Nothing is broken. gcc -c deliberately stops before linking; the .o has no entry point and unresolved symbols. Run gcc hello.o -o hello first.
Recall Two source files both define
int counter; at global scope and linking fails — but each compiled fine. What went wrong?
Each compiler saw only its own file, so neither flagged it. The linker merges symbol tables and finds a duplicate definition of counter, which it cannot place. Use one definition plus extern declarations.
Recall "I deleted
#include <stdio.h> and it still compiled with only a warning — so the header was pointless."
The header supplies the prototype; without it the compiler guesses printf's signature (implicit declaration) and only warns. The linker still resolves the real code, so it may run — but the guessed types can silently corrupt arguments.
Why questions
Recall Why split compilation into four tools instead of one big translator?
Separation of concerns: each tool solves one sub-problem, errors are localized to a stage, and libraries can be compiled once and linked into everyone — enabling fast incremental rebuilds.
Recall Why keep assembly as a visible intermediate stage at all?
It is a thin human-readable layer over machine code. Isolating it lets the compiler stay CPU-text-focused while a simple assembler does byte encoding — and it lets you inspect or hand-tune the output (e.g. across -O levels).
Recall Why does knowing
which stage complained matter for debugging? Each stage catches a different bug class: preprocessor → bad directives, compiler → syntax/type errors, linker → missing/duplicate symbols. The stage name tells you what kind of fix you need, not just that something failed.
Recall Why must macro parameters be wrapped in parentheses?
Because the preprocessor does dumb text substitution before any math is understood; without parentheses, the surrounding operators' precedence can splice into the pasted text and change the meaning.
Recall Why is the C standard library not "just built into" your
.o?
To avoid duplicating printf's code in every program. It is compiled once and linked in on demand, and dynamic linking can even share one copy across all running programs at runtime.
Edge cases
Recall What happens if a header has no include guard and is included twice (directly and via another header)?
Its declarations get pasted twice, causing "redefinition" errors at compile time. Include guards (#ifndef/#define/#endif) make the second paste expand to nothing.
Recall A file compiles with
gcc -c but the project won't link — where is the bug guaranteed not to be?
Not in that file's syntax or types — those already passed. The problem is a cross-file matter: a symbol defined nowhere, defined twice, or a library not supplied to the linker.
Recall You
#define DEBUG then wrap code in #ifdef DEBUG ... #endif. If DEBUG is never defined, what does the compiler actually see?
Nothing from that block — the preprocessor deletes it before the compiler runs, so it isn't even parsed. The code inside can contain names that don't exist and still cause no compiler error.
Recall An empty
.c file with no main — does it preprocess, compile, and assemble?
Yes: it produces a valid (empty) .o because those stages don't require an entry point. It fails only at link time when producing an executable, since _start needs main.
Recall Declared but never called:
void foo(void); at the top, foo never used anywhere. Does linking fail?
No. A declaration that is never referenced creates no undefined symbol, so the linker has nothing to resolve. The error only appears if you actually call foo without defining it.
Recall What is "relocation" and when does it matter at the boundary between stages?
Relocation is the linker assigning final memory addresses and patching every reference to them. It matters because the assembler emits code with placeholder/relative addresses — the real addresses aren't known until all .o files are merged.