Foundations — C compilation — preprocessor, compiler, assembler, linker
Before you can watch gcc hello.c transform into a program in the parent note, you need every word it uses to already mean something concrete. Below, each item is built from zero: plain meaning → the picture → why the topic needs it. Nothing is used before it is defined.
0. What is a "program", really?

Look at the figure. On the left, the amber text int main is something you understand. On the right, the cyan boxes 55 48 89 e5 … are what the CPU understands. The whole topic is the bridge between these two columns — and it needs four planks to cross: the preprocessor, then the compiler, then the assembler, then the linker, in that exact order (each defined in the callout above and expanded below).
1. The CPU and its opcodes
The picture to hold in your head: a robot that only has numbered buttons. But notice already that a button is rarely just an action — it usually also needs to know on what. On x86-64 the byte 0x55 does not mean the bare word "push"; it means the specific instruction "push the register rbp". A different register would be a different byte. So "opcode = action + which-things-it-acts-on", baked together into one (or several) bytes.
Why the topic needs it: every stage exists only because the final target is this button-pressing robot. If you forget the CPU speaks numbers — and that those numbers already carry their operands — the assembler and linker seem pointless.
2. Machine architecture (x86-64, ARM)
Picture two robot chefs from different factories: both can "stir", but chef-A's stir button is #7 and chef-B's is #12. A booklet of button-presses for one chef is gibberish to the other.
Why the topic needs it: the parent says the compiler produces assembly "specific to your CPU architecture". This is why — the same C must become different byte-streams for different chips.
3. The compiler, assembly language, and mnemonics

The mental image is a lookup, but be careful — it is not a clean one-word-to-one-byte dictionary. Look at the figure: the same mnemonic mov produces different byte sequences depending on which registers and sizes it uses. That is because the assembler must also do operand encoding:
- pick the right opcode variant for the specific registers/sizes involved,
- possibly add a prefix byte (e.g.
48marks a 64-bit operation on x86-64), - append a ModR/M byte — short for "Mod-Register/Memory", a single extra byte that spells out which registers or memory location the instruction operates on,
- and the result can be a variable number of bytes — x86 instructions range from 1 to 15 bytes.
So the assembler is a dumb-but-detailed encoder: no clever reasoning about your program's meaning, but a lot of careful, mechanical operand-to-byte bookkeeping.
Why the topic needs it: it explains why there are two separate stages (compiler → assembler). The compiler does the hard thinking (what to compute) and stops at readable mnemonics; the assembler does the detailed byte encoding. See Assembly Language Basics to go deeper.
4. Names, declarations, definitions, and linkage
The picture: a name is a luggage tag. A declaration is the tag hanging in the air with no suitcase attached yet. A definition is the tag firmly tied to a real suitcase (the code).
The picture: external symbols are luggage tags placed on the public conveyor belt where any file can grab them; static (internal) symbols are locked in a private locker — invisible outside the room.
Why the topic needs it: the linker's whole job is matching "I need this" tags to "I have this" tags. That only makes sense once you know which tags are public (external) and reachable, and which are private (internal static) and deliberately not offered. It also explains why two files can each have a static helper() with no clash — private lockers never collide.
5. Object files: sections, symbol tables, and undefined references

Picture the object file as a binder with labelled tabs: a .text tab holding code, a .data tab holding data, and a table of contents at the front (the symbol table). Some entries point to chapters printed inside (defined — cyan tick). Some are listed with a blank page and a note "fetch from elsewhere" (undefined — amber hole).
Why the topic needs it: this is the concrete reason a .o file cannot run on its own — even though it contains real machine code, its symbol table still has undefined holes, its sections have not been placed at final addresses, and it has no entry point. It is a part, not a finished machine. Whoever fills the holes and assigns addresses (the linker) turns the parts into something runnable. See Object Files and Symbol Tables.
6. Libraries, the linker, relocation, and linking time
The picture: three binders each start their .text at "address 0". Merge them and the middle binder's code really lives at, say, address 4000 now — so every internal reference must be shifted. That shift is relocation.
The picture: static linking = stapling Grandma's frosting steps into your booklet so it is self-contained; dynamic linking = leaving a sticky-note "call Grandma" and only dialling her the first time the recipe reaches that step.
Why the topic needs it: the parent says the linker "fixes up addresses" and "resolves symbols" — but when those holes are filled (link time vs. run time) is a real fork in the road. Full detail lives in Static vs Dynamic Linking.
7. The # directives (preprocessor food)
The picture: sticky-notes in the margin of your recipe saying "paste in the cake recipe here" or "whenever I write PI, I mean 3.14". An assistant obeys the sticky-notes and hands you a clean recipe with no sticky-notes left.
Why the topic needs it: #include and #define are Stage 1's entire job. Full detail lives in C Preprocessor Directives.
8. Tools, flags, and file extensions
Why the topic needs it: you cannot "predict the output of each flag" (the parent's exercise) without knowing a flag = a stop point in the pipeline. Reusing .o files is what makes Makefiles and Incremental Builds fast, and -O0..-O3 are compiler-stage flags covered in Compiler Optimization Levels (-O0..-O3).
How these foundations feed the topic
The map below is read top-down as three streams that all pour into the final pipeline. Follow each stream once:
- Stream A (bytes): the CPU eating opcodes forces the idea of an architecture-specific instruction set → which is nicknamed by assembly mnemonics → which the compiler and assembler produce and encode.
- Stream B (names): the declaration-vs-definition idea, refined by internal/external linkage, is what fills an object file's symbol table with defined and undefined entries → the linker matches those, pulling libraries in and doing relocation (at link or run time).
- Stream C (text):
#directives are what the preprocessor rewrites.
All three arrows converge on the 4-stage pipeline — the parent topic. If any single node above is fuzzy, the corresponding stage of the pipeline will feel like magic.
Equipment checklist
Recall Self-test: can you answer each before reading on?
A CPU can directly read which of {C text, opcodes}? ::: Opcodes (raw numbers) only
In one word, what is an opcode? ::: A number that means one CPU action (plus its operands)
What does each of the four tools do, in one phrase? ::: Preprocessor rewrites # text; compiler translates C to assembly; assembler encodes assembly to bytes; linker stitches object files into an executable
Does the byte 0x55 mean the bare word "push"? ::: No — on x86-64 it specifically means "push rbp"; a different register is a different byte
Is a mnemonic exactly one fixed opcode byte? ::: No — operand encoding (variant, prefix, ModR/M, variable length) means one mnemonic maps to many byte sequences
What does ModR/M stand for? ::: Mod-Register/Memory — the byte that says which registers or memory an instruction acts on
Why does the same C become different bytes on x86 vs ARM? ::: Different architectures assign different opcode numbers to the same action
Declaration vs definition in one line each? ::: Declaration = a promise a name exists; definition = the actual code behind it
Internal vs external linkage? ::: External = public, visible to other files (linker can match it); internal = static, private to its own file
What are the main sections of an object file? ::: .text (code), .data (init globals), .bss (zero globals), plus metadata and the symbol table
What do ELF and Mach-O stand for? ::: ELF = Executable and Linkable Format (Linux); Mach-O = Mach Object (macOS)
What is an undefined reference? ::: A name the file needs but has no code for — a hole to be filled by the linker
Static vs dynamic linking — when are holes filled? ::: Static = at link time (copied in); dynamic = at run time by the loader
What do the PLT and GOT enable? ::: Lazy binding — the loader resolves a dynamic symbol's real address on first use and caches it in the GOT
What is relocation? ::: Patching references to their final memory addresses after sections are merged (or when a library loads)
What starts a preprocessor directive? ::: A # at the start of the line
What does the flag -E mean to gcc? ::: Stop after the preprocessor stage
The extension after .s in the trail is? ::: .o (object file)