From Source to Running Program: Preprocessor, Compiler, Linker
Chapter Five
Syllabus topic 1, "Introduction: Algorithms, History of C, Structure of C Program. Program Characteristics, Compiler, Linker and preprocessor, pseudo code statements and flowchart symbols, Desirable program characteristics. Program structure. Compilation and Execution of a Program, C Character Set, identifiers and keywords, data types and sizes, constants and its types, variables, Character and character strings, typedef, typecasting"
Pages 19 to 24 of 222
In one line
Turning a .c file into a program you can run takes four steps in a fixed order, the preprocessor, the compiler, the assembler and the linker, and cc program.c -o program is one command that quietly runs all four.
Why it is four steps and not one
Each step exists because it does a job the others cannot.
The preprocessor works on text and understands no C at all. It pastes headers in, replaces the names you defined, and strips comments. Its whole reason for being is that a program is built out of pieces kept in different files, and something has to assemble the pieces into one stream of text before anybody tries to make sense of it.
The compiler is the part that understands C. It reads the text, checks that it is a legal program, and translates it into instructions for one particular kind of processor.
The assembler turns those instructions from text into the numbers a processor actually reads.
The linker exists because your file is not the whole program. You called printf, and you did not write printf. Somebody compiled it years ago and it sits in a library on your machine. The linker's job is to find every name your file uses but does not contain, fetch the machine code for it, and join everything into one file the operating system can load.
Splitting the work this way is what lets a large program be compiled one file at a time, and it is why a mistake gets caught by a different tool depending on what kind of mistake it is. That last point is worth more marks than any other in this chapter.
The program we will push through all four stages
#include <stdio.h>
#define SIDE 7
int main(void)
{
printf("Area of a square of side %d is %d\n", SIDE, SIDE * SIDE);
return 0;
}Area of a square of side 7 is 49Nine lines, saved as area.c. Now watch what each stage does to it.
Stage 1: the preprocessor
cc -std=c17 -E area.c > area.i-E means stop after preprocessing. The result, by convention a .i file, is still C text you can read. Its size is the first surprise:
$ wc -l < area.c
9
$ wc -l < area.i
554Nine lines became 554. Almost all of that is stdio.h, pasted in whole, bringing the declarations of every standard input and output function with it. The end of the file is your own code, and it has changed:
$ tail -6 area.i
# 5 "area.c"
int main(void)
{
printf("Area of a square of side %d is %d\n", 7, 7 * 7);
return 0;
}From Source to Running Program: Preprocessor, Compiler, Linker
Three things happened, and each one tells you what the preprocessor is.
SIDEbecame7. Both times. That is all#definedoes: replace a name with a piece of text.SIDE SIDEbecame7 7, and not49. The preprocessor does not do arithmetic. It moved text around and stopped. The multiplication is the compiler's job, and in fact the compiler does it before the program ever runs, because both operands are constants.#includeand#defineare gone, along with the blank line structure, replaced by line markers like# 5 "area.c"that let the compiler report errors against your file rather than against the 554-line stream.
If a comment had been in the file, it would be gone too. The preprocessor removes comments, which is why a comment costs nothing at run time.
Stage 2: the compiler
cc -std=c17 -S area.c -o area.s-S means stop after compiling, and leave the result as assembly language: text, one instruction per line, for one specific processor. The nine-line program becomes 35 lines of it, beginning like this:
$ sed -n '1,11p' area.s
.arch armv8-a
.file "area.c"
.text
.section .rodata
.align 3
.LC0:
.string "Area of a square of side %d is %d\n"
.text
.global main
.type main, %function
main:You are not expected to read assembly on this paper. Three things in it are worth seeing once.
.arch armv8-a names the processor family. This listing was produced on arm64, and the same program compiled in a lab with an Intel processor produces a different listing that does the same thing. Assembly is not portable; C is. That is the whole argument for a compiled high-level language.
.string "Area of a square of side %d is %d\n" is your text, stored as data. The format string was not turned into instructions, because it is not an instruction. It is a run of characters the program will hand to printf.
main: is a label, and it marks the place where your function's instructions begin. The linker will later look for exactly that name.
Stage 3: the assembler
cc -std=c17 -c area.c -o area.o-c means compile and assemble, but do not link. The result is an object file, and it is the first thing in this chain you cannot read as text: it holds machine code. What you can read is its table of names, with nm:
$ nm area.o
0000000000000000 T main
U printfTwo lines, and they are the whole idea of linking.
T mainmeans this file defines a function calledmain, in its text (code) section, at offset 0.Tis for text.U printfmeans this file uses a function calledprintfand does not define it.Uis for undefined. There is a hole in the machine code where a call toprintfshould go, and this file cannot fill it.
From Source to Running Program: Preprocessor, Compiler, Linker
The object file is 1,648 bytes on the machine that produced these numbers. Your own will differ, and the number does not matter. What matters is that it is small, because it holds your nine lines and nothing else.
Stage 4: the linker
cc -std=c17 area.o -o areaNow the hole gets filled. The linker takes your object file, finds printf in the standard C library, copies in the machine code for it and for everything printf itself needs, adds the small piece of start-up code that calls main for you, and writes one executable file.
$ stat -c %s area
70312
$ ./area
Area of a square of side 7 is 491,648 bytes of object file became a 70,312 byte program. Your nine lines are a tiny fraction of what you are running, and the rest is library and start-up code you did not write and did not have to.
All four in one command
cc -std=c17 -Wall -Wextra area.c -o areaThis is what you will actually type. It runs the preprocessor, the compiler, the assembler and the linker in order, keeps none of the intermediate files, and stops at the first stage that fails. -o area names the output; without it the program is called a.out, which is a habit from the earliest Unix and has stuck for fifty years.
Then run it. The ./ is not decoration: it means "in this directory", and without it the shell looks for area in the standard places and does not find it.
./areaWhich stage caught your mistake
This is the useful half of the chapter. An error message tells you which tool is complaining, and therefore what kind of thing is wrong. All five below are real messages from gcc 13.3.
The preprocessor cannot find a header. Misspell stdio.h and the chain stops before any C is looked at:
$ cc -std=c17 e1.c -o e1
e1.c:1:10: fatal error: stdioo.h: No such file or directory
1 | #include <stdioo.h>
| ^~~~~~~~~~The compiler finds a broken statement. Leave out a semicolon and the compiler complains, and notice where: it names line 5, although the missing semicolon is on line 4. The compiler discovered the problem when it read the next thing that could not follow.
$ cc -std=c17 e2.c -o e2
e2.c: In function 'main':
e2.c:5:5: error: expected ',' or ';' before 'return'
5 | return a;
| ^~~~~~The linker cannot find a definition. Declare a function, call it, and never write it. Nothing is wrong with your C, so the compiler is satisfied, and the linker is the one that objects:
From Source to Running Program: Preprocessor, Compiler, Linker
$ cc -std=c17 e3.c -o e3
/usr/bin/ld: in function `main':
e3.c:(.text+0xc): undefined reference to `twice'
collect2: error: ld returned 1 exit statusThe linker cannot find main. Compile a file with functions but no main, and the start-up code is the thing left with a hole:
$ cc -std=c17 e4.c -o e4
(.text+0x1c): undefined reference to `main'
collect2: error: ld returned 1 exit statusA warning is not an error, and matters more. Delete #include <stdio.h> and the program still links and still runs, and gcc tells you that you are calling a function it knows nothing about:
$ cc -std=c17 -Wall -Wextra nohdr.c -o nohdr
nohdr.c:6:5: warning: implicit declaration of function 'printf' [-Wimplicit-function-declaration]
nohdr.c:1:1: note: include '<stdio.h>' or provide a declaration of 'printf'That is the class of mistake -Wall -Wextra exists to surface. Compile with them always. A program that runs today with a warning is a program that breaks on somebody else's compiler.
| Symptom | Stage | Typical cause |
|---|---|---|
No such file or directory on a header | Preprocessor | Header name misspelled or not installed |
expected ';', undeclared identifier, type errors | Compiler | Grammar or type mistake in your C |
undefined reference to 'x' | Linker | Declared or called but never defined, or a library not named |
undefined reference to 'main' | Linker | No main in anything being linked |
implicit declaration of function | Compiler, as a warning | A missing #include |
| Nothing at all, wrong answer | None | Your logic. No tool can catch this |
What this does NOT mean
The compiler does not produce a runnable program. It produces an object file with holes in it. Only the linker produces something you can run. Saying "the compiler turns C into an executable" is the single commonest error on this topic.
A header is not a library. stdio.h is text: declarations telling the compiler what printf looks like. The machine code for printf lives in a compiled library and is joined on by the linker. This is why a missing header gives a compiler warning while a missing library gives a linker error.
The preprocessor is not part of the C language. It has its own grammar, does not understand types, expressions or scope, and would happily replace a name inside something you never meant. Chapter 22 shows what that costs.
An interpreter is not doing this. In an interpreted language there is no separate translation step and no executable. C compiles ahead of time, which is why a C program starts instantly and why you must compile again after every edit.
cc and gcc are not necessarily different programs. On the machine these transcripts came from, cc is gcc 13.3 reached through another name. On a Mac, cc is usually clang. cc is the portable name, which is why this book uses it.
From Source to Running Program: Preprocessor, Compiler, Linker
Quick revision
- Four stages in order: preprocessor, compiler, assembler, linker.
- Preprocessor: text only. Pastes
#include, substitutes#define, strips comments. No arithmetic, no type checking. Stop after it with-E. - Compiler: checks the C and emits assembly for one processor. Stop after it with
-S. - Assembler: assembly text into machine code, giving an object file. Stop after it with
-c. - Linker: joins object files and libraries, resolves every undefined name, adds start-up code, writes the executable.
nmon an object file showsT namefor defined andU namefor used but undefined.undefined referenceis always the linker.expected ';'is always the compiler.cc -std=c17 -Wall -Wextra file.c -o filethen./file.
Test yourself
1. Put these in order and say what each produces: linker, assembler, preprocessor, compiler.
Preprocessor, giving expanded C text. Compiler, giving assembly. Assembler, giving an object file. Linker, giving an executable.
2. You get undefined reference to 'average'. Which stage failed, and what are the two likely causes?
The linker. Either you declared or called average and never defined it, or its definition is in another file or library that you did not include in the link command.
3. A program prints the wrong answer. Which stage will find the bug?
None. Every stage succeeded; the program you described is the program you wrote. This is what tracing by hand and testing are for.
4. What does -E do, and why is the output so much longer than the input?
It stops after preprocessing. The output is longer because #include <stdio.h> pastes the whole header in, which is several hundred lines of declarations.
5. After #define N 5, does N * N reach the compiler as 25?
No. It reaches the compiler as 5 * 5. The preprocessor substitutes text and does no arithmetic. The compiler then works out 25 while compiling, because both operands are constants.
6. Why does ./area need the ./?
Because the shell searches only a fixed list of directories for a command name, and the current directory is not normally on that list. ./area says explicitly which file to run.
What can be asked on this, and how to answer it
"Explain the compilation and execution of a C program." Name the four stages in order, say what each takes in and gives out, and name the file at each boundary: .c, then expanded text, then .s, then .o, then the executable. Finish with the run step. A diagram of five boxes and four arrows earns the marks quickly; label the arrows with the tool and the boxes with the file.
From Source to Running Program: Preprocessor, Compiler, Linker
"What is the role of the linker?" To resolve names. It joins your object files with the library code for every function you used but did not write, adds the start-up code that calls main, and produces a single executable. Give undefined reference as the error it reports when it cannot.
"Distinguish between the compiler and the interpreter." A compiler translates the whole program once, ahead of time, into machine code, which then runs on its own and runs fast; errors in the whole file are reported before anything runs. An interpreter translates and executes statement by statement every time the program runs, needs the interpreter present to run at all, and reports an error only when execution reaches it.
"What is the purpose of the preprocessor?" To process the file as text before compilation: insert headers, substitute defined names, select code conditionally, and remove comments. Say plainly that it does not understand C, because that is the part examiners are testing.
"A header file and a library file, distinguish." A header is source text holding declarations, read by the compiler so it can check your calls. A library is compiled machine code holding definitions, read by the linker so your calls have something to reach. stdio.h against the C standard library is the example to give.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.