Chapter 6
From source to a running program
You type one command and get a program. It looks like one action. It is four, and clang will stop after any of them and show you the result. This chapter follows a single file through every stage.
The program we will follow
Make a file called sum.c with this in it:
#include <stdio.h>
int main()
{
int num1;
int num2;
printf("To add two numbers.\n");
printf("Enter the first number: ");
scanf("%i", &num1);
printf("Enter the second number: ");
scanf("%i", &num2);
num1 = num1 + num2;
printf("The sum of two numbers is %i.\n", num1);
return 0;
}
Twenty-one lines. Keep that number in mind — it grows in a moment.
The simplest way to compile
Give clang the file and nothing else:
$ clang sum.c
Nothing is printed. Now look at the folder:
$ ls
a.out sum.c
A new file has appeared, called a.out. Run it:
$ ./a.out
To add two numbers.
Enter the first number: 3
Enter the second number: 4
The sum of two numbers is 7.
It stands for assembler output. The name is over fifty years old and has stayed as the default ever since. It tells you nothing about your program, which is exactly the problem with it.
Name your program
Nobody ships a.out. The -o option says what the
finished program should be called:
$ clang sum.c -o sum
$ ./sum
Do this every time. Two programs in one folder both called
a.out cannot both exist, and a month later you will not know
what a.out was built from.
One command, four stages
That single command did four separate jobs, one after another, and threw away everything in between.
| Stage | Does what | In | Out |
|---|---|---|---|
| 1. Preprocess | Pastes in other files, handles lines starting with # |
.c |
.i |
| 2. Compile | Turns C into instructions for this processor | .i |
.s |
| 3. Assemble | Turns those instructions into numbers | .s |
.o |
| 4. Link | Joins your code to the code it borrows | .o |
your program |
You can keep all the middle files instead of throwing them away:
$ clang --save-temps sum.c -o sum
$ ls
sum sum.bc sum.c sum.i sum.o sum.s
There they all are. Now let us make each one on purpose, and look inside it.
Stage 1 — the preprocessor
-E tells clang to preprocess and then stop.
$ clang -E sum.c -o sum.i
Count the lines in what came out:
$ wc -l sum.i
847 sum.i
Your twenty-one lines became eight hundred and forty-seven. That is what
#include <stdio.h> did. The preprocessor found that file
and pasted its entire contents into yours, and everything
stdio.h includes as well.
Your own code is still there, right at the end:
$ tail -n 5 sum.i
printf("The sum of two numbers is %i.\n", num1);
return 0;
}
It only moves text about. It pastes files in, replaces names you defined
with #define, and drops parts you switched off. It has no idea
what a function or a variable is.
That is why an error inside a header file can produce a strange message. By the time the compiler complains, that text is part of your file.
Stage 2, part one — clang's own language
Before clang writes instructions for your processor, it writes them in a half-way language of its own. It is called LLVM IR. IR stands for intermediate representation — a step in the middle.
$ clang -S -emit-llvm sum.c -o sum.ll
$ wc -l sum.ll
47 sum.ll
Open it and you will find your main function, written differently:
define dso_local i32 @main() #0 {
i32 means a 32-bit whole number — the int that
main returns.
This middle language is not tied to any processor. It is where clang does its thinking and its improvements. The same IR can then be turned into instructions for an Intel chip, an ARM chip, or something else entirely.
--save-temps gave you sum.bc rather than
sum.ll. They hold the same thing. .bc is the
compact form for machines, .ll is the readable form for
people.
Stage 2, part two — assembly
-S stops after the compiler has produced instructions for
your processor, written as text.
$ clang -S sum.c -o sum.s
$ head -n 8 sum.s
.text
.file "sum.c"
.globl main
.p2align 4, 0x90
.type main,@function
main:
.cfi_startproc
# %bb.0:
This is assembly: one line for roughly one instruction the processor can carry out. It is the last stage a person can comfortably read.
Notice the very first line, .text. That is the name of the
section your code goes into. It matters again shortly.
Assembly is written for one kind of processor. On a normal laptop you will
see Intel instructions like pushq and movq. On an
ARM machine the same C produces completely different words.
The C file did not change. Only the processor it is being aimed at did.
Stage 3 — the assembler makes an object file
-c compiles and assembles, then stops before linking.
$ clang -c sum.c -o sum.o
sum.o is called an object file. It holds real
machine instructions, as numbers. But it is not a program yet, and the system
knows it:
$ file sum.o
sum.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
The word is relocatable, not executable. Something is still missing, and you can see exactly what:
$ nm sum.o
U __isoc99_scanf
U printf
U means undefined. Your file uses
printf and scanf, but it does not contain them. You
never wrote them. They live in the C library, somewhere else on the machine.
Stage 4 — the linker finishes the job
The linker finds that missing code and joins everything together.
$ clang sum.o -o sum
$ file sum
sum: ELF 64-bit LSB pie executable, x86-64, dynamically linked, ...
Now it says executable. Compare the sizes, and you can see work was done:
| File | Size | What it is |
|---|---|---|
sum.o | 1856 bytes | your code only, with holes in it |
sum | 16064 bytes | a program that can start on its own |
The extra is the code that runs before main and after it — the
part that sets things up, calls your main, and hands its answer
back to the system.
A message that mentions undefined reference is the linker speaking. Your code compiled perfectly. The linker then went looking for something you called, and could not find it anywhere.
What is inside the program
Your program is not one lump. It is split into sections, by kind.
$ size sum
text data bss dec hex filename
1609 592 8 2209 8a1 sum
| Section | Holds |
|---|---|
| text | the code — the instructions the processor will carry out |
| data | values that are known before the program starts, such as your messages |
| bss | room set aside for values that begin as zero |
"text" is an old name for the code section. You saw it earlier as the first
line of sum.s.
Your messages really are sitting in the file, and you can find them:
$ strings sum | grep numbers
To add two numbers.
The sum of two numbers is %i.
What happens when you type ./sum
The program is a file on the disk. Running it takes four steps.
- The shell asks the operating system to run the file.
- The system reads the sections out of the file and puts them into RAM. The code goes in one place, the data in another.
-
The system finds the address of
mainand puts it into a register inside the processor. That register holds the address of the next instruction to carry out. It is called the program counter on some processors and the instruction pointer on others. Same job, different name. - The processor reads the instruction at that address, carries it out, moves to the next one, and keeps going.
That is the whole idea. Chapter 1 said the CPU reads one instruction, does it, and reads the next. Now you know where those instructions came from and how the processor was pointed at the first one.
The addresses your program uses are not really addresses in the memory chips. Every program gets its own set of numbers, and the machine quietly maps them onto real memory. It is called virtual memory.
It means two programs can both use the address 4096 and never touch each other's data. A later chapter takes this apart properly. For now, the picture above is close enough and correct in shape.
Where the exit code comes from
Look at the last line of your program:
return 0;
That is not decoration. main hands that number back to the
system, and the system passes it to your shell. It is the
exit code from chapter 4:
zero for success, anything else for a problem.
$ ./sum
To add two numbers.
Enter the first number: 3
Enter the second number: 4
The sum of two numbers is 7.
$ echo $?
0
Prove the link. Change that one line to return 3;, build again, and run it:
$ clang sum.c -o sum
$ ./sum
$ echo $?
3
The number you wrote in your C file came back out of the shell. That is the
same number && and || look at, and the same
number the setup scripts check after every step.
Try it
Build the whole thing one stage at a time and look at each result:
$ clang -E sum.c -o sum.i && wc -l sum.i
$ clang -S -emit-llvm sum.c -o sum.ll && wc -l sum.ll
$ clang -S sum.c -o sum.s && wc -l sum.s
$ clang -c sum.c -o sum.o && file sum.o
$ clang sum.o -o sum && file sum
Then compare the three text files. Same program, three languages:
$ wc -l sum.c sum.i sum.ll sum.s
You can now explain
- Why
clang sum.cproduces a file calleda.out, and why to use-oinstead - The four stages, and which option stops clang after each one
- What
#includeactually does, and why 21 lines became 847 - What LLVM IR is, and why a middle language is useful
- Why an object file is "relocatable" and not yet a program
- What
U printfmeans, and what the linker does about it - What the code and data sections hold
- What the system does when you type
./sum - Where the exit code comes from, and how to change it
