C Part I · Module 1 · Understanding C and How Programs Work
From Source Code to Executable
Chapter 1.5 left two loose threads on purpose. The single cc command you typed is not one operation, and the failure table at the end referred to a tool you had not met. This chapter pulls both. A typical C toolchain does its work in four stages, and each one leaves behind a file you can open and read — so rather than describing the pipeline, we will run it one stage at a time and look at what comes out. Along the way two beginner beliefs get retired: that one line of C becomes one machine instruction, and that your executable contains all the code it needs to run. Before any of that, though, one distinction has to be straight: this four-stage arrangement is what real toolchains do, not what the C standard demands, and the difference matters.
Foundation16 min readCPart IFundamentalsToolchain
Part I · Module 1 · Chapter 1.6 · From Source Code to Executable
1. One Command Was Not One Operation
In Chapter 1.5 you typed something like this and got a program:
cc -std=c17 -Wall -Wextra -Wpedantic hello.c -o helloThat reads like a single instruction, and it is convenient that it behaves like one. But cc is a driver: a program whose job is to run several other programs in order and pass the results along. What you called was the coordinator, not the work.
This chapter takes the command apart. You can ask cc to stop after each stage and keep what it produced, which means the whole pipeline is something you can watch rather than something you have to imagine.
2. What the Standard Requires, and What Your Toolchain Does
Before the walk-through, one distinction — because getting it backwards is how people end up with a confident but wrong model of C.
The C standard does not require four programs. It describes translation in terms of ordered phases, and it requires that preprocessing happen before the rest of translation. It does not say that a compiler must be several programs, that assembly text must ever exist, or that a file called an "object file" must be produced. An implementation only has to behave as if the required order had been followed.
What follows is what nearly every real toolchain actually does. It is worth learning for two reasons that have nothing to do with the standard: each stage leaves a file you can inspect, and — more usefully — each stage produces its own kind of error, so knowing the stages tells you where to look.
So read the next five sections as a description of your tools, not of the language.
3. Stage 1 — Preprocessing
The preprocessor runs first, and it works on your source text. Ask cc to stop after it:
cc -std=c17 -E hello.c -o hello.i-E means "preprocess only". Now compare the two files. On the machine this chapter was written on:
hello.c 7 lines, 84 bytes
hello.i 573 lines, 25,795 bytesSeven lines became five hundred and seventy-three. That is what #include <stdio.h> did.
Chapter 1.4 said #include "brings in a description" of functions like printf, and warned that how it does that is more literal than the phrase suggests. Here is the literal version: the preprocessor found the file stdio.h and pasted its entire contents into your file, in place of the #include line. That file includes others, which were pasted in too, and so on until there was nothing left to paste.
Now look at the end of hello.i:
int main(void)
{
printf("Hello, world!\n");
return 0;
}Your code, unchanged, sitting at the bottom of five hundred lines of declarations that arrived from elsewhere. That is the whole of preprocessing in this program: a large amount of text inserted above your code, and your code left exactly as you wrote it.
4. Stage 2 — Compiling, and the Myth It Retires
Now the part that actually knows C. It takes the preprocessed file — the translation unit, the whole of what the compiler proper works on, which Chapter 11.2 defines properly — and produces instructions for one particular kind of processor.
cc -std=c17 -S hello.c -o hello.s-S means "stop after compiling, and give me the instructions as text". The result on this machine was 30 lines. Here are a few of them, from the middle:
_main:
sub sp, sp, #32
stp x29, x30, [sp, #16]
add x29, sp, #16
mov w8, #0
str w8, [sp, #8]You are not expected to read this, and this curriculum will never ask you to write it. Look at it for one reason only.
Chapter 1.1's comparison table described assembly language as text naming individual machine operations for one processor family. That is what this is — and this is where it comes from. Your portable C became instructions for the specific machine you compiled on, which is precisely the translation Chapter 1.2 said C exists to make possible.
And now compare the two. Your main contained two statements: a printf call and a return. Several of the instructions above are needed before that call has even happened, and the excerpt stops well before the end.
5. Stage 3 — Assembling
The assembler turns those instructions into the numeric form the processor actually takes, and writes it to an object file:
cc -std=c17 -c hello.c -o hello.o-c means "compile and assemble, but do not link". The result was 744 bytes — smaller than the assembly text it came from, because numbers are more compact than the words that describe them.
An object file is machine code, but it is not runnable, and the reason is the whole subject of the next stage: it still contains unfinished business. Your main calls printf, and nothing in your source says where printf is. The object file records that as a question rather than an answer.
6. Stage 4 — Linking, and Where printf Actually Comes From
The linker takes one or more object files, resolves the references they left open, and produces a file the system can run:
cc hello.o -o helloNotice what is absent from that command: no -std=c17, no warning options. Those are instructions for the compiler, and the compiler has already finished. This is a different tool with a different job — which is exactly the tool Chapter 1.5's failure table referred to without naming.
Now, the question Chapter 1.4 deferred: where does printf's code come from?
The intuitive answer is that the linker copies it into your executable. On the machine used for this chapter, that is not what happened. The finished executable was 33,432 bytes, and it still lists printf as something it does not contain. What the linker did was record that the program requires a library which supplies printf, and leave the actual joining until the program is run.
That is the common arrangement, and it has a name — dynamic linking — but the name matters less than the consequence:
Your executable is not necessarily self-contained. It can be a complete, finished, runnable file that still depends on code found elsewhere at the moment it starts.
Which means "the executable exists" and "the program can run" are two different claims. Chapter 1.7 takes that apart.
7. A Linker Error Looks Nothing Like a Compiler Error
Here is a program that is perfectly valid C and cannot be built:
int compute_checksum(int value);
int main(void)
{
return compute_checksum(7);
}The first line tells the compiler that a function called compute_checksum exists, takes a whole number and produces one. That is a promise, and the compiler is entitled to believe it. main then calls it.
The compiler has no complaint whatsoever. Every line is legal, the call matches the promise, and compiling succeeds. Then:
Undefined symbols for architecture arm64:
"_compute_checksum", referenced from:
_main in linkerr-27e82c.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1Compare this with the compiler diagnostics in Chapter 1.5 and notice how different it is:
- No file and no line number. The linker is not looking at your source; it never saw it.
- It names a symbol — a name the linker was required to find — rather than a statement.
compute_checksumis what was missing. ldis the linker's own name appearing in its own message.- It says where the reference came from — inside
main, in an object file — because that is the level it works at.
The promise was never kept. Somebody declared a function and no one ever wrote it, and the only stage that could possibly notice is the one that has to find it. Module 11.9 treats these properly, once you are writing programs made of several files and this failure becomes routine.
8. Which Stage Did My Error Come From?
This table is the practical payoff of the whole chapter:
| Symptom | Stage | What it means |
|---|---|---|
| Complaint about a file that is not yours, or a missing header | Preprocessing | A file could not be found, or something pasted in did not fit |
| A file, a line and a character, with a description of bad C | Compiling | Your source is not valid C, or is suspicious enough to warn about |
| An unresolved or undefined symbol, with no line number | Linking | Something was promised and never defined, or a library was not supplied |
| The program built, then failed or misbehaved | Running | Nothing was wrong with the build. Chapter 1.7, and Module 13 |
A beginner who calls all four "a compiler error" looks in the wrong place three times out of four. Reading the shape of the message first is most of the skill.
9. What You Should Be Able to Say Now
ccis a driver that runs several programs in order; one command is not one operation.- The four-stage arrangement — preprocess, compile, assemble, link — is what real toolchains do. The standard requires the order of translation, not this arrangement.
- Preprocessing pastes included files into your source, literally. Seven lines became 573, and your own code was left untouched.
- The preprocessor does not understand C; it works on tokens, before anything checks whether the result is sensible.
- Compiling turns portable C into instructions for one particular processor, and there is no fixed correspondence between lines of C and instructions.
- An object file is machine code that is not yet runnable, because it still contains unresolved references.
- Linking resolves those references — which does not always mean copying the code in. An executable can depend on code supplied when it runs.
- Compiler errors carry a file and line; linker errors carry a symbol and no line. They are different tools failing at different times.
10. Exercise
Exercise 1 — attribute the stage. For each, name the stage that produces the complaint and give a one-line reason:
(a) fatal error: 'stdioo.h' file not found
(b) undefined symbol: _parse_header
(c) error: expected ';' after return statement
(d) The program builds and prints the wrong number.
Exercise 2 — predict the file sizes. You add a second #include line for another standard header to hello.c and preprocess it again. What happens to the line count of the preprocessed file, and what happens to the part of it that contains your own main?
Exercise 3 — explain the asymmetry. Chapter 1.5's compile command had -std=c17 and three warning options; §6's link command had none of them. Explain why that is correct rather than an oversight.
Exercise 4 — reason about the promise. In §7 the compiler accepted a call to a function that did not exist anywhere. Explain what the compiler was relying on, and why it is reasonable for it to be satisfied when the linker is not.
Exercise 5 — retire the myth in your own words. A colleague says: "Each line of C becomes a couple of machine instructions, so I can estimate a program's speed by counting lines." Using §4, give two distinct reasons that reasoning fails.
Exercise 6 — order the artifacts. Put these in the order they are produced, and say which one is the first that could be run: hello.o, hello, hello.c, hello.i, hello.s.
11. Summary
- One
cccommand runs four programs.ccis a driver; preprocessing, compiling, assembling and linking are separate stages, and each can be stopped at with-E,-S,-c, or by linking on its own. - The pipeline is your toolchain's arrangement, not the language's rule. The standard requires an order of translation and permits an implementation to reach the same result any way it likes.
#includeis literal. The preprocessor pastes the named file's whole contents into yours — 7 lines became 573 — and leaves your own code alone.- There is no line-to-instruction correspondence. Three lines of C produced many instructions, which is Chapter 1.1's "behaviour, not steps" made visible.
- An object file is unfinished machine code, holding references it cannot resolve on its own.
- Linking resolves references, not necessarily by copying. Your executable may be complete and still depend on library code that arrives when it runs.
- Errors have shapes. A file and a line means the compiler; a symbol and no line means the linker. Knowing which saves you looking in the wrong place.
The pipeline ends with a file, and §6 ended by driving a wedge into a word we have been using loosely. An executable exists; a program runs. Those are not the same thing, the gap between them is where the missing library code gets found, and main — as Chapter 1.4 hinted twice — is not the first thing that happens. The last chapter of this module is about that gap.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
