History, Standards, and the Compilation Model
Last week you typed one command and got a program. This week you open that command up. Along the way, the history explains why C makes the choices it does, and the standards explain why the same source file can mean different things to different compilers.
- Explain why C was created and what problem it solved for the Unix authors.
- Name the major C standards and say what each one added.
- Run the preprocessor, compiler, assembler, and linker separately and inspect each output.
- Choose a language standard deliberately with
-std=instead of accepting the default. - Explain why a program that compiles for a classmate might not compile for you.
1Where C came from
In 1969 the Bell Labs researchers Ken Thompson and Dennis Ritchie were building an operating system on a spare minicomputer. That system became Unix, and it was written, as all operating systems then were, in assembly language. Assembly is fast and precise, but it is tied to one processor: rewriting Unix for a different machine would have meant rewriting it entirely.
Thompson had already written a small language called B, itself a stripped-down version of BCPL, which came from Cambridge. B had one significant limitation: every value was a single machine word. It had no notion of a byte, a character, or a floating-point number. When the lab acquired a PDP-11 — a machine with byte addressing — B no longer fit the hardware.
Ritchie's response, between 1971 and 1973, was to add a type system to B. Values now had sizes: char, int, float, double. Structures arrived. The result was called C, and in 1973 Unix was rewritten in it. That was the decisive moment: an operating system in a portable language could be moved to a new machine by writing a new compiler rather than a new operating system.
Why this matters for you. C was designed to be a portable assembler for systems programming in the early 1970s, on machines with a few dozen kilobytes of memory. Almost every feature you will find odd — no bounds checking, null-terminated strings, arrays decaying into pointers, undefined behavior — follows from that goal and those constraints. Week 42 returns to this once you have enough C to appreciate the reasoning.
2From a book to a standard
For its first decade, C had no formal definition. What it had instead was a book: The C Programming Language by Brian Kernighan and Dennis Ritchie, published in 1978. Programmers call that dialect K&R C. The book described the language accurately, but a book is not a specification: where it was silent or ambiguous, compiler vendors each decided for themselves, and C began to fragment.
ANSI convened a committee in 1983 to settle the question. The result, published in 1989, is ANSI C, adopted internationally the following year as ISO C. Everyone calls it C89 or C90; they are the same language. This standard introduced the function prototype — declaring a function's parameter types so the compiler can check calls against them — which is the single largest safety improvement in C's history.
Since then the language has been revised roughly once a decade by working group WG14. Revisions are conservative: C is a language where a twenty-year-old source file is expected to still compile.
What each revision added
| Standard | Year | Notable additions |
|---|---|---|
| C89 / C90 | 1989 | Function prototypes, void, const, volatile, a standard library definition. The baseline every C compiler supports. |
| C99 | 1999 | // comments, declarations anywhere in a block, long long, _Bool with <stdbool.h>, fixed-width types in <stdint.h>, designated initializers, compound literals, variable-length arrays, inline, restrict, snprintf. |
| C11 | 2011 | Threads (<threads.h>), atomics (<stdatomic.h>), _Generic, _Static_assert, _Alignas and _Alignof, _Noreturn, anonymous structures and unions. |
| C17 / C18 | 2018 | No new features. A maintenance release that fixed defects in C11. If you target C17 you are targeting C11 with the bugs corrected. |
| C23 | 2024 | bool, true, and false as keywords, nullptr, constexpr, binary literals such as 0b1010, digit separators, typeof, attributes such as [[nodiscard]], and the removal of old K&R-style function definitions. |
A practical consequence: most of the "modern C" conveniences you will read about online — declaring a loop variable inside the for, using // for a comment — are C99 features. They are twenty-five years old and universally available, but they are not in C89, and some embedded toolchains still default to C89.
3The four translation phases
When you run gcc -o hello hello.c, GCC is acting as a driver: it runs four separate programs in sequence and quietly deletes the intermediate files. Here is what each one does, and how to stop after it.
Each flag stops the driver after one stage and leaves the intermediate file behind.
Phase 1 — the preprocessor
The preprocessor is a text manipulator. It knows nothing about C. It obeys the lines beginning with #: it pastes in the contents of included files, substitutes macros, and strips comments. Its output is still C source — just much longer.
gcc -E hello.c -o hello.i
wc -l hello.iExpect several hundred to a few thousand lines from a program you wrote five of. Everything after the last # directive is your code; everything before it came from stdio.h and the headers it includes in turn. Scroll to the bottom and you will find your main unchanged.
Phase 2 — the compiler proper
This is the stage people mean by "compiling": preprocessed C in, assembly language out. All the type checking, all the optimization, and all the interesting diagnostics happen here.
gcc -S hello.c -o hello.s
cat hello.sYou are not expected to read assembly yet. Look at it anyway. Find the string Hello, world sitting in a data section, and find the call instruction that invokes printf. Recognizing that your program became this is worth more than understanding every line. Week 34 comes back and reads it properly.
Phase 3 — the assembler
The assembler turns assembly text into a binary object file containing real machine instructions.
gcc -c hello.c -o hello.o
file hello.o
nm hello.onm lists the symbols in the object file. You will see T main — a symbol this file defines in the text section — and U printf, a symbol this file uses but does not define. That single letter U is the reason the next phase exists.
Phase 4 — the linker
The linker collects object files and libraries, resolves every undefined symbol against a definition somewhere, and writes one executable.
gcc hello.o -o hello
./helloThis is where printf gets connected to the real implementation inside the C standard library. It is also where the undefined reference error from week 1 came from: the linker searched everywhere it was told to look and found no definition.
4Worked example: the same file, two standards
Save this as era.c. It deliberately uses features from different decades.
#include <stdio.h>
int main(void)
{
/* A C89-era comment. */
int total = 0;
for (int i = 0; i < 5; i++) { /* declaration inside for: C99 */
total += i;
}
printf("total = %d\n", total);
return 0;
}Compile it as C89:
gcc -std=c89 -Wall -Wextra -o era era.cera.c: In function 'main':
era.c:8:5: error: 'for' loop initial declarations are only allowed in C99 or C11 mode
8 | for (int i = 0; i < 5; i++) {
| ^~~
era.c:8:5: note: use option '-std=c99', '-std=c11' or '-std=gnu99' to compile your codeNow as C23:
gcc -std=c23 -Wall -Wextra -o era era.c
./eratotal = 10The file did not change. The language changed. This is worth sitting with: "valid C" is not a single fixed thing, and the question "does this compile?" is incomplete until you say which standard you meant.
Now walk it through the phases
gcc -std=c23 -E era.c -o era.i && wc -l era.i
gcc -std=c23 -S era.c -o era.s && grep -c . era.s
gcc -std=c23 -c era.c -o era.o && nm era.o
gcc era.o -o era && ./eraTwo things to notice. First, the comment you wrote is gone by the time you look at era.i — the preprocessor removed it, which is why comments cost nothing at run time. Second, the loop may have vanished from era.s entirely if you add -O2: the compiler can compute 0+1+2+3+4 at compile time and emit the constant 10. Try it.
gcc -std=c23 -O2 -S era.c -o era-opt.s
grep -n '10\|\$10' era-opt.sWhat just happened. The compiler is not a transcriber. It is allowed to produce any machine code whose observable behavior matches what the standard says your program means. That freedom is what makes C fast, and it is also the reason undefined behavior is so dangerous — a topic week 36 is entirely about.
5Choosing your flags
Settle on a command line now and use it for the rest of the course:
gcc -std=c17 -Wall -Wextra -g -o prog prog.c| Flag | Why |
|---|---|
-std=c17 | States the language explicitly. Without it you get the compiler's default, which differs between compilers and versions, and which is usually a GNU dialect rather than standard C. |
-Wall | Turns on the common warnings. The name is misleading: it is not all of them. |
-Wextra | Turns on a further set that -Wall omits. Together these two catch a large share of beginner bugs. |
-g | Embeds debugging information so GDB can show you source lines and variable names. Costs nothing at run time. |
-O0 … -O2 | Optimization level. -O0 (the default) while developing, because the debugger behaves predictably; -O2 when measuring speed. |
-pedantic | Warns about anything outside the standard you named. Worth switching on once you are comfortable. |
gnu17 is not c17. GCC's default is -std=gnu17, which is C17 plus GNU extensions. Code that relies on those extensions compiles happily for you and fails for someone using a different compiler. Naming the standard explicitly turns a confusing future bug report into a compile error you see today.
6Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
Using // comments under -std=c89 | C++ style comments are not allowed in ISO C90 | Use /* … */, or target C99 or later. |
| Declaring a variable in the middle of a block under C89 | ISO C90 forbids mixed declarations and code | Same fix. This is the most common reason old toolchains reject modern code. |
Assuming -E output is an error | Thousands of lines scroll past | That is the point. Redirect it: -o file.i. |
Forgetting -o with -c | Output lands in hello.o anyway | Harmless here, but be deliberate: name your outputs. |
Debugging a -O2 build and finding variables "optimized out" | GDB cannot show a value that no longer exists | Debug at -O0. Week 35 explains why optimized builds confuse debuggers. |
Believing the C standard specifies int as 32 bits | Code breaks on another platform | It specifies a minimum range. Week 37 covers what is actually guaranteed. |
7Check yourself
Why was rewriting Unix in C such a significant event?
Because it decoupled the operating system from the processor. An assembly-language system must be rewritten for each new machine; a C system needs only a C compiler for that machine. Portability through a language, rather than through discipline, is the idea C was built to deliver.
Your classmate's program compiles on their laptop and fails on yours with "'for' loop initial declarations are only allowed in C99 or C11 mode". What differs?
The language standard each compiler defaulted to, not the code. Their compiler defaults to a C99-or-later dialect; yours is being invoked in C89 mode. The real fix is not to change the code but for both of you to pass -std= explicitly so the question never arises.
What is the difference between -c and -S?
-S stops after the compiler proper and leaves human-readable assembly in a .s file. -c goes one stage further, running the assembler, and leaves a binary object file .o. Neither runs the linker, so neither produces a program you can execute.
nm hello.o shows U printf. What does the U mean and what will resolve it?
U means undefined: this object file refers to printf but does not contain its body. The linker resolves it by finding the definition in the C standard library, which it searches automatically. For other libraries you must say so — -lm for the math library, for example.
You compile with -O2 and the loop disappears from the assembly. Is this a bug?
No. The compiler must preserve your program's observable behavior, not its literal structure. If it can prove the loop always produces 10, it may emit the constant. This latitude is what makes optimizing compilers effective — and what makes undefined behavior so hazardous, because a program with undefined behavior gives the compiler a false premise to reason from.
8Where this leads
You now know what the build does and which language you are writing. Week 3 turns to the program itself: the anatomy of main, what its return value is for, how to write code that another person can read, and how to put your work under version control so that week 43's lesson on navigating someone else's codebase starts from a habit you already have.