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 main function. It returns an integer, the program's exit status: 0 if all went well, something else otherwise. The shell keeps it in $?.
  • void between the parentheses says that main takes no argument. We will see in chapter 6 the version that receives the command line.
  • printf is 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!
OptionEffect
-o hellonames the executable hello rather than a.out
-std=c11follows the 2011 C standard (without it, gcc accepts its own extensions)
-Wallturns on the common warnings (the name is misleading: not all of them)
-Wextraadds some more
-Werrorturns every warning into an error: the program does not compile as long as any remain
-O2optimises (-O0: no optimisation, -O3: more aggressive)
-gkeeps 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 file stdio.h;
  • #define TAILLE 100 has every following TAILLE replaced by 100;
  • #if, #ifdef, #ifndef … #endif keep 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 iscopied into the executablestays in the library, loaded at start-up
Size of the executablelargersmaller
On the machine that runs itnothing to installthe library must be present
If ten programs use itten copies in memorya single one, shared
Fixing a bug in the libraryevery program must be relinkedreplacing 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:

SectionWhat it holdsReadWriteExecuteIn the file
.textthe machine code of the functionsyesnoyesyes
.rodataconstants, string literalsyesnonoyes
.datainitialised global and static variablesyesyesnoyes, with their value
.bssglobal and static variables with no initialisation, or set to zeroyesyesnono, 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.