Version without JavaScript, read-only: workshops, quizzes and running code are not available here. Full version · Catalogue · Sign in
Lecture notes · chapter 1
Compiling and running
From the source file to the running program - preprocessor, compiler, assembler, linker and loader. The sections of the executable (text, rodata, data, bss) and their permissions. gcc's options, programs in several files, header files and make.
A first program
Hello World
#include <stdio.h>
int main(void)
{
printf("Hello World!\n");
return 0;
}
We save it in hello_world.c, and compile it with gcc:
$ ls
hello_world.c
$ gcc hello_world.c
$ ls
a.out hello_world.c
$ ./a.out
Hello World!
gcc has produced an executable, a file of machine code that the system knows how to load and start. Unless told otherwise, it is called a.out. It must be started with ./a.out: the shell only looks for commands in the directories of the PATH variable, and the current directory is not one of them.
A few remarks about this program:
- Every C program starts with the
mainfunction. It returns an integer, the program's exit status:0if all went well, something else otherwise. The shell keeps it in$?. voidbetween the parentheses says thatmaintakes no argument. We will see in chapter 6 the version that receives the command line.printfis not a statement of the language, it is a function of the standard library. The line#include <stdio.h>makes its declaration visible.
Useful options
$ gcc -std=c11 -Wall -Wextra -Werror -O2 -o hello hello_world.c
$ ./hello
Hello World!
| Option | Effect |
|---|---|
-o hello | names the executable hello rather than a.out |
-std=c11 | follows the 2011 C standard (without it, gcc accepts its own extensions) |
-Wall | turns on the common warnings (the name is misleading: not all of them) |
-Wextra | adds some more |
-Werror | turns every warning into an error: the program does not compile as long as any remain |
-O2 | optimises (-O0: no optimisation, -O3: more aggressive) |
-g | keeps the debugging information, for gdb and valgrind |
For example, without #include <stdio.h>:
int main(void)
{
printf("oups\n");
return 0;
}
$ gcc -std=c11 -Wall -Wextra oubli.c
oubli.c: In function 'main':
oubli.c:3:5: warning: implicit declaration of function 'printf'
oubli.c:1:1: note: include '<stdio.h>' or provide a declaration of 'printf'
The compiler came across a call to a function it does not know. So it knows neither what arguments it expects nor what it returns. Without -Werror, it still produces an executable, which works here by luck.
From source to execution
The word “compile” actually covers several steps, each done by a different tool. gcc chains them all for us, but we can do them one by one.
flowchart LR
A[hello.c] -->|preprocessor| B[pure source]
B -->|compiler| C[hello.s]
C -->|assembler| D[hello.o]
D -->|linker| E[a.out]
L[libraries] --> E
E -->|loader| P[process]The preprocessor
It handles the lines that start with #, and only deals with text: it knows nothing about C.
#include <stdio.h>is replaced by the contents of the filestdio.h;#define TAILLE 100has every followingTAILLEreplaced by100;#if,#ifdef,#ifndef…#endifkeep or remove pieces of the text: this is conditional compilation.
gcc -E stops after this step and prints the result. Our seven lines become more than eight hundred: all of stdio.h, and what it includes itself.
$ gcc -E hello_world.c | wc -l
820
The compiler
This is the heart of it: it translates C into assembly, the processor's textual language. gcc -S stops there and produces hello_world.s:
main:
pushq %rbp
movq %rsp, %rbp
leaq .LC0(%rip), %rax
movq %rax, %rdi
call puts@PLT
movl $0, %eax
popq %rbp
ret
You can recognise the string, stored separately under the label .LC0, and the function call. The compiler has even replaced printf with puts, which is simpler, since the string contains no %.
The assembler
It translates the assembly into machine code, in an object file .o. gcc -c stops there. An object file is not yet executable: it contains the code of our functions, and the list of those it uses without having them. nm prints that list:
$ gcc -c hello_world.c
$ nm hello_world.o
0000000000000000 T main
U puts
T: main is defined here, in the code (text). U: puts is used, but undefined, defined elsewhere.
The linker
It gathers the object files and the libraries, and links each use of a function to its definition. It produces the executable. If a definition is missing, it is the linker that complains, not the compiler:
void dire_bonjour(void);
int main(void)
{
dire_bonjour();
return 0;
}
$ gcc lien.c
/usr/bin/ld: /tmp/ccuTbmNs.o: in function `main':
lien.c:(.text+0x9): undefined reference to `dire_bonjour'
collect2: error: ld returned 1 exit status
The compiler was happy: we had declared dire_bonjour to it, so it knew how to call it. But nobody defined it, and the linker (ld) cannot find its code anywhere.
The loader
At start-up, the system loads the executable into memory, links it to the dynamic libraries it needs, then runs it. The running program is called a process (chapter 6).
Static and dynamic libraries
A library is a set of functions that are already compiled: the C standard library (the libc) contains printf, malloc, strlen… It can be linked in two ways.
| Where the code is | copied into the executable | stays in the library, loaded at start-up |
|---|---|---|
| Size of the executable | larger | smaller |
| On the machine that runs it | nothing to install | the library must be present |
| If ten programs use it | ten copies in memory | a single one, shared |
| Fixing a bug in the library | every program must be relinked | replacing the library is enough |
By default, gcc links the libc dynamically. ldd prints the libraries an executable will need:
$ ldd a.out
linux-vdso.so.1
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
/lib64/ld-linux-x86-64.so.2
$ gcc -static hello_world.c -o hello_static
$ ls -l a.out hello_static
-rwxrwxr-x 1 philr philr 15960 a.out
-rwxrwxr-x 1 philr philr 785360 hello_static
ld-linux-x86-64.so.2 is the loader itself. The static version weighs fifty times more: it carries everything printf needs.
Inside the executable: the sections
An object file, and the executable after it, is not a uniform block: it is organised in sections, one per kind of content. At start-up, the loader places each one at its spot in memory, and gives it permissions: read (r), write (w), execute (x). The processor enforces them on every access. Four sections matter to the programmer:
| Section | What it holds | Read | Write | Execute | In the file |
|---|---|---|---|---|---|
.text | the machine code of the functions | yes | no | yes | yes |
.rodata | constants, string literals | yes | no | no | yes |
.data | initialised global and static variables | yes | yes | no | yes, with their value |
.bss | global and static variables with no initialisation, or set to zero | yes | yes | no | no, only their size |
None is both writable and executable. Ordinary local variables are in none of them: they only exist on the stack, during the call, and the areas from malloc are in the heap (chapter 3).
What goes where
#include <stdio.h>
const char salut[] = "bonjour"; /* constant : .rodata */
int compteur = 42; /* initialised : .data */
int tableau[1000]; /* no initialisation, so zero : .bss */
int main(void)
{
static int appels; /* static local, zero : .bss */
appels++;
compteur++;
tableau[0] = compteur;
printf("%s %d %d %d\n", salut, compteur, tableau[0], appels);
return 0;
}
nm gives the section of each symbol as a letter, upper case for a symbol visible to other files, lower case for a local symbol:
$ gcc -c sections.c
$ nm sections.o
0000000000000fa0 b appels.0
0000000000000000 D compteur
0000000000000000 T main
U printf
0000000000000000 R salut
0000000000000000 B tableau
T for text, R for read-only data, D for data, B for bss. The local variable appels is stored like a global, since static gives it the lifetime of the program, but under a private name (appels.0) that the other files do not see.
size -A gives the size of each section:
$ size -A sections.o
sections.o :
section size addr
.text 112 0
.data 4 0
.bss 4004 0
.rodata 21 0
.comment 46 0
.note.GNU-stack 0 0
.note.gnu.property 32 0
.eh_frame 56 0
Total 4275
The last four lines are information for the tools (compiler version, stack unwinding for the debugger), which do not concern the program.
.text: the code
.text contains the instructions of all the functions: here the 112 bytes of main. The processor can only execute what is in a page marked x, and this is the only section that is.
It is read-only: a program cannot modify its own code. This protects against mistakes, since a stray pointer cannot overwrite a function, and against attacks, which precisely try to get bytes they have written executed. Since it never changes, the system can also share it: ten processes running the same program have only one copy of it in memory.
.rodata: the constants
.rodata (read-only data) contains what the program reads without ever modifying it: the global variables declared const, and the string literals. Here 21 bytes: "bonjour" (8, with its '\0') and the printf format, "%s %d %d %d\n" (13).
It is read-only, and not executable. It is the system, not the compiler, that guarantees a constant does not change: if you strip the const by force, the program compiles, but dies at the first write. That is why char *s = "bonjour"; s[0] = 'B'; crashes (chapter 3).
.data: the initialised variables
.data contains the global and static variables that have a non-zero initial value: here the 4 bytes of compteur, which is 42. That value is written in the file, and the loader copies it into memory at start-up.
It is read-write: the program modifies these variables freely. So each process has its own copy, which it shares with no one.
.bss: the zero variables
.bss contains the global and static variables with no initialisation, or initialised to zero: here tableau and appels, 4,004 bytes. The standard requires them to be zero at start-up.
Since everything in it is zero, the file does not need to contain these bytes: it only records the size of the section, and at start-up the loader reserves the memory, filled with zeros. The object file above weighs 2,000 bytes in all, less than the array alone. You can check this by giving a value to a single cell, int tableau[1000] = {1};: the array moves into .data, and its thousand integers go into the file.
$ size sections gros
text data bss dec hex filename
1471 604 4040 6115 17e3 sections
1471 4648 8 6127 17ef gros
$ ls -l sections gros
-rwxrwxr-x 1 philr philr 20136 gros
-rwxrwxr-x 1 philr philr 16104 sections
Without the -A option, size groups the sections into three columns, and counts .rodata with the code in text, since both are read-only. The executable's figures exceed those of the object file: the linker has added, around main, the program's start-up code and its data.
Once loaded, .bss behaves exactly like .data: read-write. This is why in C an uninitialised global or static variable is zero at no cost, whereas an uninitialised local variable contains anything at all. The name comes from a 1950s assembler: Block Started by Symbol.
The permissions, checked
On Linux, /proc/self/maps describes the memory of the process that reads it: one line per area, with its addresses and its permissions. This program prints an address from each section, then the first lines, those of the executable:
#include <stdio.h>
const char salut[] = "bonjour";
int compteur = 42;
int tableau[1000];
int main(void)
{
printf(".text main %p\n", (void *)main);
printf(".rodata salut %p\n", (void *)salut);
printf(".data compteur %p\n", (void *)&compteur);
printf(".bss tableau %p\n", (void *)tableau);
fflush(stdout);
FILE *f = fopen("/proc/self/maps", "r");
char ligne[256];
for (int i = 0; i < 5 && fgets(ligne, sizeof ligne, f) != NULL; i++)
fputs(ligne, stdout);
fclose(f);
return 0;
}
.text main 0x5b03e4c61209
.rodata salut 0x5b03e4c62008
.data compteur 0x5b03e4c64010
.bss tableau 0x5b03e4c64040
5b03e4c60000-5b03e4c61000 r--p ... droits
5b03e4c61000-5b03e4c62000 r-xp ... droits
5b03e4c62000-5b03e4c63000 r--p ... droits
5b03e4c63000-5b03e4c64000 r--p ... droits
5b03e4c64000-5b03e4c65000 rw-p ... droits
(The paths are shortened, and the addresses change on every run.) main falls in the r-x page: read and execute. salut in an r-- page: read-only. compteur and tableau in the rw- page: read and write, and no page is both w and x. The first r-- page contains the file header; the fourth, data that the dynamic linker fills in at start-up and then write-protects.
Writing into each section confirms these permissions:
#include <stdio.h>
#include <string.h>
const char salut[] = "bonjour"; /* .rodata */
int compteur = 42; /* .data */
int tableau[1000]; /* .bss */
int main(int argc, char *argv[])
{
compteur = 7; /* .data : allowed */
tableau[999] = 7; /* .bss : allowed */
printf("data %d, bss %d\n", compteur, tableau[999]);
if (argc > 1 && strcmp(argv[1], "rodata") == 0) {
char *p = (char *)salut; /* strip the const by force */
p[0] = 'B';
printf("rodata modifiée : %s\n", salut);
}
if (argc > 1 && strcmp(argv[1], "text") == 0) {
unsigned char *code = (unsigned char *)main;
code[0] = 0xc3;
printf("code modifié\n");
}
return 0;
}
$ ./ecrire
data 7, bss 7
$ ./ecrire rodata
data 7, bss 7
Segmentation fault (core dumped)
$ ./ecrire text
data 7, bss 7
Segmentation fault (core dumped)
The compiler accepted everything: the casts tell it to keep quiet. It is the system that stops the program, at the first write into a page that does not allow it. Neither of the two printf calls that follow runs. (Converting the address of a function into a data pointer is not allowed by the standard, -pedantic flags it; it is accepted on Linux, and handy for this demonstration.)
Several files
Splitting a program
A real program does not fit in one file. It is split into modules, each with two files:
- a source file
.c, which defines the functions (their code); - a header file
.h, which declares them (their prototype), so that the other files can use them.
bonjour.h:
#ifndef BONJOUR_H
#define BONJOUR_H
void dire_bonjour(void);
#endif
bonjour.c:
#include <stdio.h>
#include "bonjour.h"
void dire_bonjour(void)
{
printf("Bonjour !\n");
}
aurevoir.h and aurevoir.c are built the same way, with a function dire_au_revoir. Finally salut.c:
#include "bonjour.h"
#include "aurevoir.h"
int main(void)
{
dire_bonjour();
dire_au_revoir();
return 0;
}
#include <…> looks for the file in the system directories; #include "…" looks for it first next to the source file. Quotes are used for your own headers.
Include guards
The three lines #ifndef BONJOUR_H, #define BONJOUR_H and #endif are an include guard. The first time the preprocessor reads the file, BONJOUR_H is not defined: it keeps the contents, and defines BONJOUR_H. The following times, BONJOUR_H is defined: the whole contents are skipped.
Without it, a header included twice (directly, or because two other headers include it) would make its type definitions appear twice, which the compiler rejects. Every header file has its guard, with a name of its own.
Compiling separately
We produce one object file per source file, then link them:
$ gcc -std=c11 -Wall -Wextra -Werror -c bonjour.c # produces bonjour.o
$ gcc -std=c11 -Wall -Wextra -Werror -c aurevoir.c # produces aurevoir.o
$ gcc -std=c11 -Wall -Wextra -Werror -c salut.c # produces salut.o
$ gcc -o salut salut.o bonjour.o aurevoir.o
$ ./salut
Bonjour !
Au revoir !
The benefit shows when you modify a file. If you change the message in aurevoir.c, you only need to redo aurevoir.o and the link: salut.c and bonjour.c have not changed, their object files are still good. On a program of a few thousand files, that is the difference between one second and half an hour.
You still have to know what to recompile. If you modify bonjour.h, you must recompile bonjour.c, but also salut.c, which includes it. That is make's job.
make
A first Makefile
make reads a file called Makefile, which describes rules:
target: prerequisites
<TAB>command
To produce the target, make runs the command, but only if the target does not exist or if one of its prerequisites is newer than it. And it first applies the same logic to each prerequisite.
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -Werror -O2
salut: salut.o bonjour.o aurevoir.o
$(CC) -o salut salut.o bonjour.o aurevoir.o
salut.o: salut.c bonjour.h aurevoir.h
$(CC) $(CFLAGS) -c salut.c
bonjour.o: bonjour.c bonjour.h
$(CC) $(CFLAGS) -c bonjour.c
aurevoir.o: aurevoir.c aurevoir.h
$(CC) $(CFLAGS) -c aurevoir.c
clean:
rm -f salut *.o
CC and CFLAGS are variables: you define them once, and use them with $(…). The first rule in the file is the one make runs by default; make clean runs the clean rule, which has no prerequisite and produces no file.
$ make
gcc -std=c11 -Wall -Wextra -Werror -O2 -c salut.c
gcc -std=c11 -Wall -Wextra -Werror -O2 -c bonjour.c
gcc -std=c11 -Wall -Wextra -Werror -O2 -c aurevoir.c
gcc -o salut salut.o bonjour.o aurevoir.o
$ ./salut
Bonjour !
Au revoir !
We modify aurevoir.c, and run it again:
$ make
gcc -std=c11 -Wall -Wextra -Werror -O2 -c aurevoir.c
gcc -o salut salut.o bonjour.o aurevoir.o
$ make
make: 'salut' is up to date.
Only aurevoir.o was rebuilt, then the executable. On the second call, everything is up to date: there is nothing to do.
Going further
make already knows some implicit rules: it knows how to build a .o from the .c of the same name, with $(CC) $(CFLAGS) -c. So we could just write the dependencies. It also has automatic variables ($@ for the target, $^ for all the prerequisites), pattern rules, and can work out by itself which headers each file includes. To find out more: Using GNU Make, by Marwan Burelle, and the GNU Make manual.