Step 63. The Compilation Process — Four Workers Passing the Baton
Level 1 — Programming and the Computer’s Inner Workings | Difficulty ★★★☆☆ | Estimated time: 3 hours
Prerequisites: Steps 56–62 complete. You can compile C programs with gcc.
- What you need: a WSL Ubuntu terminal, gcc, nano (or cat). The verification environment is Ubuntu 24.04, gcc 13.3.0, x86-64.
- Caution: today’s exercises are 100% safe. We only create and transform files — no system work that deletes or modifies anything.
So far we’ve compiled with a single line, gcc hello.c -o hello. Like a magic incantation. But behind that one line, four workers are passing a baton. Today we open the lid of that workbench. The first harvest is that your eyes for reading error messages change; the second is that knowing "how an executable is born" becomes the foundation for the later study of dissecting executables.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Recite in order the four stages behind
gcc hello.c -o hello(preprocessing, compilation, assembly, linking) - Reproduce each stage separately with the
-E,-S,-coptions and keep the artifacts (.i, .s, .o, executable) - Open each artifact and explain its role
- Know which stage an "undefined reference" error speaks for, and prescribe the fix
- Translate a two-file project piece by piece and link it at the end
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | C language, WSL Ubuntu bash, gcc 13.x (verified: 13.3.0, x86-64) |
| Today’s commands | gcc -E (preprocess only), gcc -S (up to assembly), gcc -c (up to object), gcc -save-temps (keep intermediate artifacts), file (check a file’s identity), nm (see names inside a part), ldd (list libraries) |
| Concepts needed | Preprocessing, compilation, assembly, linking, object files, libraries, the ELF format |
2-1. The Four-Stage Workbench
The journey of C source code to an executable.
- Preprocessing: handles directives starting with
#.#includeunfolds the contents of that file right into place, and#definesubstitutes. The result is a.ifile. - Compilation: translates the unfolded C code into assembly (the human-readable edition of machine code). The result is a
.sfile. "Compilation" in the narrow sense refers to only this stage. - Assembly: transcribes assembly into machine code (0s and 1s). The result is a
.o(object) file. Machine code in parts condition, not yet runnable. - Linking: combines several object files and libraries (other people’s parts like printf) into a runnable finished product. The stage that connects the "this function lives over there" links between parts.
2-2. Object Files — Machine Code as Parts
A .o file is a lump of machine code that’s finished translating but can’t run alone. The part with main, the part with add, the library part with printf — only when the linker bundles these into one lump does it become an executable.
This structure is why a large program can be split across hundreds of files.
2-3. Why Run It in Stages
Split into parts, you can retranslate only the files you fixed and reuse the rest. Retranslating a million-line program in one go every time would take all day.
And thanks to this structure, the act of "attaching a part someone else made (a library) to my program" becomes possible.
2-4. ELF — The Format of Linux Executables
Linux object files and executables follow a common format called ELF (Executable and Linkable Format). "Parts condition" and "finished product" are, so to speak, different states of the same format.
Today we confirm this format name ourselves with the file command.
3. Follow Along
3-1. Preparing the Experiment Source
Input (stage.c)
#include <stdio.h>
#define VERSION 2
int main(void) {
printf("Stage-by-stage experiment, version %dn", VERSION);
return 0;
}
How to read it: it contains both #include and #define. Experiment material for seeing what happens to both in the preprocessing stage.
3-2. Stage 1: Preprocess Only
gcc -E stage.c -o stage.i
wc -l stage.i
tail -5 stage.i
820 stage.i
# 4 "stage.c"
int main(void) {
printf("Stage-by-stage experiment, version %dn", 2);
return 0;
}
(Verified 2026-09-09. The line count varies by environment.)
New commands: gcc -E is "preprocess only and stop." wc -l is "count the lines," tail -5 is "show the last five lines."
How to read the output: our code was eight lines, but the artifact is 820 lines. Because stdio.h’s contents were all unfolded into it. And look at our main at the very bottom — a 2 is stamped where VERSION was: the #define was already substituted. Search with grep -c "define VERSION" stage.i and you get 0 hits. Once preprocessing is done, no # directives remain.
Why: the moment you confirm with hundreds of lines of output that #include was "copy-paste," the true identity of header files becomes clear.
3-3. Stage 2: Translate to Assembly
gcc -S stage.i -o stage.s
grep -nE "main:|call" stage.s
9:main:
22: call printf@PLT
(Verified 2026-09-09.)
How to read the output: -S is "translate only up to assembly." Our C changed into a list of instructions like mov and call. You don’t need to understand all of it. Just find this much: "ah, it says call printf. That’s the function we called." (@PLT is a marker of the gateway for calling library functions — for now, reading as far as "it calls printf" is enough.)
3-4. Stage 3: Making the Machine-Code Part
gcc -c stage.s -o stage.o
file stage.o
stage.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
(Verified 2026-09-09.)
How to read the output: -c is "up to the object only." The file command tells you this file’s identity. ELF (2-4’s format), relocatable (a parts-condition file whose addresses aren’t fixed yet). It’s machine code but a half-finished product that can’t run alone. Open this file and you’ll only see garbled characters — it’s not for humans.
3-5. Stage 4: Completing with the Link
gcc stage.o -o stage
./stage
Stage-by-stage experiment, version 2
(Verified 2026-09-09.)
How to read the output: we linked one object file into an executable and ran it. Here our part merged with the standard library part where printf lives. We ran all four stages by hand. The single line gcc stage.c -o stage was these four called in sequence.
3-6. Splitting into Two Files — Linking’s Reason to Exist
Input (math_ops.c)
int add(int a, int b) {
return a + b;
}
Input (main2.c)
#include <stdio.h>
int add(int a, int b);
int main(void) {
printf("40 + 2 = %dn", add(40, 2));
return 0;
}
Compile and run
gcc -c math_ops.c -o math_ops.o
gcc -c main2.c -o main2.o
gcc main2.o math_ops.o -o calc
./calc
40 + 2 = 42
(Verified 2026-09-09.)
How to read the output: we made each of the two sources into a part (each with -c), then linked the two together at the end. The int add(int a, int b); at the top of main2.c is a declaration saying "a function of this shape exists somewhere." main2.o was translated trusting those words, and the linker found that somewhere in math_ops.o and connected it.
Predict: what error appears if you leave out math_ops.o in the final link and run only
gcc main2.o -o calc? Predict, then check yourself. This is Wall 1’s true identity.
Why: this is how large programs work. Hundreds of files are each translated into .o files and linked all at once at the end.
3-7. One-Step Confirmation — All Four at Once with -save-temps
If running the four stages one by one is tedious, gcc has an option to leave all intermediate artifacts behind.
gcc -save-temps stage.c -o stage2
ls stage2*
stage2 stage2-stage.i stage2-stage.o stage2-stage.s
(Verified 2026-09-09. Attaching -o stage2 makes the artifact names take the form stage2-stage.*. Without -o, just gcc -save-temps stage.c leaves stage.i, stage.s, stage.o.)
How to read the output: along with the executable, the .i, .s, and .o all remain. The four workers each left their artifacts on the workbench. Now say to yourself which stage each file belongs to. If you can say it, you’ve understood today.
Why: knowing a tool’s shortcut and knowing the process are different things. Today we learned the process first; the shortcut comes after. Reverse the order and the shortcut becomes magic.
3-8. The Parts Box Called a Library
Where did printf come from? We never wrote it, yet it works once linked. The answer: "a giant parts box called the standard C library is already installed, and the linker found the printf part there and attached it."
nm stage.o
ldd stage
0000000000000000 T main
U printf
linux-vdso.so.1 (0x00007fd6fd414000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd6fd000000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd6fd416000)
(Verified 2026-09-09. ldd’s address values differ on every run.)
How to read the output:
nmshows the list of names inside a part.T mainmeans "a part called main is in here (T = defined)";U printfmeans "printf is not here and must be fetched from somewhere (U = undefined)."lddshows "which parts boxes this executable uses." Seelibc.so.6? The standard library where printf lives.
Just as Python’s import finds tools at run time, a C executable carries a list of the boxes it needs. This is the true meaning of the word "link" — not merging but connecting.
4. Missions & Exercises
Mission — A Four-Stage Reproduction Report
- Make your own source (including two functions, e.g.,
int square(int n)andmain) asreport.c - Reproduce each of the four stages with commands, and keep all four artifacts (.i, .s, .o, executable)
- Open the
.sfile and find and mark your function’s name and thecallinstruction - On purpose, link with one part left out, like
gcc main2.o -o calc2, to reproduce an "undefined reference" error, and copy the full error into your notes - Answer in the report: for each of the four stages, (a) the input, (b) the artifact, (c) what it does — one line each
Exercises
Problem 1. In 3-2, an eight-line source became an 820-line .i. What came in, and why is the #define VERSION 2 line not left in the .i?
Problem 2. Up to which stage do gcc -E, gcc -S, and gcc -c each go, and what are their artifacts’ extensions?
Problem 3. In nm stage.o, printf had a U beside it. What does this mean, and at which stage is that printf ultimately fetched from where?
Problem 4. The error "undefined reference to ‘add’" — which of the four workers speaks it? And give two representative causes.
5. Model Answers & Completion Criteria
Mission Model Answer
An example of the reproduction commands:
gcc -E report.c -o report.i # stage 1: preprocess
gcc -S report.i -o report.s # stage 2: translate to assembly
gcc -c report.s -o report.o # stage 3: object part
gcc report.o -o report # stage 4: link
./report
The full text of the deliberately caused error (verified 2026-09-09):
/usr/bin/ld: main2.o: in function `main':
main2.c:(.text+0x13): undefined reference to `add'
collect2: error: ld returned 1 exit status
How to verify: ① all four artifacts must remain in the folder. ② grep -nE "main:|call" report.s must find the function name and the call line. ③ The error text must show ld (the linker) and undefined reference. ④ Filling in the four-stage table: preprocessing — input .c, artifact .i, job "handle directives (unfold, substitute)" / compilation — input .i, artifact .s, job "translate to assembly" / assembly — input .s, artifact .o, job "turn into a machine-code part" / linking — input .o files + libraries, artifact executable, job "connect the parts."
Exercise Answers
Problem 1 answer. #include <stdio.h> unfolded that file’s entire contents into place. The #define was already substituted (VERSION → 2) in the preprocessing stage, so the directive itself doesn’t remain in the artifact.
Problem 2 answer. -E goes up to preprocessing (artifact .i), -S up to assembly translation (.s), and -c up to the object part (.o). The final linking stage has no separate option.
Problem 3 answer. U means "undefined — not inside this part, must be fetched from somewhere." printf is fetched and connected from the parts box called the standard C library (libc.so.6) at the linking stage. You can confirm that box with ldd.
Problem 4 answer. It’s the linker’s words. It translated believing "this function must be somewhere," and at link time it couldn’t find that definition. Representative causes: ① you left out an .o part that should have been passed along; ② the function name’s spelling differs among the three places — declaration, definition, and call.
Completion Criteria Checklist
- [ ] I can recite the four compilation stages (preprocessing, compilation, assembly, linking) in order
- [ ] I reproduced each stage with
-E,-S,-c, and no option, and kept the artifacts - [ ] I can find substitution results in a .i and call instructions in a .s
- [ ] I can check the identity of parts and executables with
file,nm,ldd - [ ] I can explain the stage (linking) and fix for an "undefined reference" error
- [ ] Mission: I completed the four-stage reproduction report and the error reproduction record
6. Common Pitfalls & Fixes
Wall 1. undefined reference to ‘function name’
Symptom: linking fails with an error like this (verified 2026-09-09):
main2.c:(.text+0x13): undefined reference to `add'
collect2: error: ld returned 1 exit status
Cause: at the linking stage, the function’s actual definition couldn’t be found. You left out a part, or the function name differs between declaration and definition.
Fix: check "did I pass all the .o files together," and "is the function name spelled the same in all three places — declaration, definition, call."
Wall 2. Flustered Because the Artifact Isn’t Human Text
Symptom: you open a .o file and are startled by heaps of garbled characters.
Cause: a .o is machine code. It’s not for humans.
Fix: that’s normal. The way to read it is with tools like file (identity check) and nm (see the function names inside).
Wall 3. Confusing the -E, -S, -c Options
Symptom: you mix up which option goes up to which stage.
Cause: the three options look alike.
Fix: remember by the artifacts. -E is .i (unfold), -S is .s (assembly), -c is .o (part). Not alphabetical order — the journey’s order. "Unfold (E), translate (S), make a part (c), and bundle at the end (no option)."
Wall 4. Missing Header File Error
Symptom: fatal error: stdio.h: No such file or directory appears (in a Korean-locale environment the message may appear translated).
Cause: an environment without the development header files installed (happens on minimal Linux installs). A preprocessing-stage error.
Fix: install the development tool bundle with sudo apt install build-essential -y. gcc and the essential headers come together.
Wall 5. -save-temps Artifact Names Differ from Expectations
Symptom: you expected stage2.i but got stage2-stage.i (verified 2026-09-09).
Cause: with -o stage2 attached, gcc derives the artifact names from the output file name.
Fix: without -o, just gcc -save-temps stage.c leaves stage.i, stage.s, stage.o. Both are normal — check the actual names with ls.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| Preprocessing | The stage that unfolds #include and substitutes #define → .i |
| Compilation (narrow sense) | The stage that translates C into assembly → .s |
| Assembly | The stage that transcribes assembly into machine-code parts → .o |
| Linking | The stage that connects parts and libraries into a finished product → executable |
| Object file | Machine code in parts condition, unable to run alone (relocatable) |
| Library | A box containing other people’s parts like printf (libc) |
| ELF | The common format of Linux objects and executables |
Today’s Commands
| Command | What it does |
|---|---|
gcc -E source.c -o result.i |
Preprocess only (unfold headers) |
gcc -S result.i -o result.s |
Up to assembly |
gcc -c result.s -o result.o |
Up to object (part) |
gcc result.o -o executable |
Link into the finished product |
gcc -save-temps source.c |
Keep all intermediate artifacts |
file file |
Check a file’s identity |
nm part.o |
List names inside a part (T = defined, U = undefined) |
ldd executable |
List required libraries |
A Sense More Important Than Commands
From today, when you see a C error message, the first question is "which worker is speaking." "File not found (stdio.h)" is preprocessing speaking, "expected ;" is compilation speaking, "undefined reference" is the linker speaking. Know the worker and the prescription diverges. Errors are long and in English, but once you know their affiliation, the part you must read shrinks dramatically.
One more thing. When files number in the dozens, you can’t type 3-6’s list of commands every time. So there’s an automation tool, make, that "retranslates only what changed and links it too." For now, just know it exists — the day your files pass five, you’ll seek it out yourself.
Once every box is checked, Step 63 is complete.