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.
Why "a.out"

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.

StageDoes whatInOut
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;
}
The preprocessor does not understand C

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.

Two files, one idea

--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.

Your file will not match this exactly

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.

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:

FileSizeWhat it is
sum.o1856 bytesyour code only, with holes in it
sum16064 bytesa 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.

This is the stage that produces the strangest errors

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
SectionHolds
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.

  1. The shell asks the operating system to run the file.
  2. The system reads the sections out of the file and puts them into RAM. The code goes in one place, the data in another.
  3. The system finds the address of main and 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.
  4. 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.

A simplification, on purpose

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