Beaucoup de concepts sont les même qu’en Java
Interface d’un module: partie publique accessible d’autres modules
Implémentation d’un module: partie privée
Bibliothèque (en anglais: library) : regroupe un ensemble de modules
Deux fichiers par module. Par exemple, pour le module
mem_alloc
:
mem_alloc.h
(fichier
d’entête / header)
mem_alloc.c
mem_alloc.h
:
#include "mem_alloc.h"
mem_alloc.h
mem_alloc
(depuis le module
main
)
mem_alloc.h
:
#include "mem_alloc.h"
mem_alloc.h
mem_alloc
mem_alloc
/* mem_alloc.h */
#include <stdlib.h>
#include <stdint.h>
#define DEFAULT_SIZE 16
typedef int64_t mem_page_t;
struct mem_alloc_t {
/* [...] */
};
/* Initialize the allocator */
void mem_init();
/* Allocate size consecutive bytes */
int mem_allocate(size_t size);
/* Free an allocated buffer */
void mem_free(int addr, size_t size);
/* mem_alloc.c */
#include "mem_alloc.h"
struct mem_alloc_t m;
void mem_init() { /* ... */ }
int mem_allocate(size_t size) {
/* ... */
}
void mem_free(int addr, size_t size) {
/* ... */
}
#include "mem_alloc.h"
int main(int argc, char**argv) {
();
mem_init/* ... */
}
Compilation en trois phases:
exécutable
Le préprocesseur transforme le code source pour le compilateur
#define N 12
substitue N
par
12
dans le fichier#if <cond> ... #else ... #endif
permet la
compilation conditionnelle#ifdef <var> ... #else ... #endif
(ou l’inverse:
#ifndef
) permet de ne compiler que si var
est
défini (avec #define
)#include "fichier.h"
inclue (récursivement) le fichier
"fichier.h"
gcc -E mem_alloc.c
La directive #if
permet, par exemple, de fournir
plusieurs implémentations d’une fonction. Cela peut être utilisé pour
des questions de portabilité.
#if __x86_64__
void foo() { /* implementation pour CPU intel 64 bits */ }
#elif __arm__ /* équivalent à #else #if ... */
void foo() { /* implementation pour CPU ARM */ }
#else
void foo() {
printf("Architecture non supportée\n");
abort();
#endif
Il y a deux syntaxes pour la directive #include
: *
#include <fichier>
: le préprocesseur cherche
fichier
dans un ensemble de répertoires systèmes
(/usr/include
par exemple) *
#include "fichier"" le préprocesseur cherche
fichier` dans
le répertoire courant, puis dans les répertoires systèmes.
On utilise donc généralement #include "fichier"
pour
inclure les fichiers d’entête définis par le programme, et
#include <fichier>
pour les fichiers d’entête du
système (stdio.h
, stdlib.h
, etc.)
Compilation : transformation des instructions C en instructions “binaires”
gcc -c mem_alloc.c
mem_alloc.o
Edition de liens : regroupement des fichiers objets pour créer un exécutable
gcc -o executable mem_alloc.o module2.o [...] moduleN.o
Règles de compilations
mem_alloc.c
, il est nécessaire de regénérer le fichier
objet (mem_alloc.o
), et de regénérer l’exécutable. Il n’est
toutefois pas nécessaire de recompiler les modules utilisant le module
modifié.mem_alloc.h
), il est nécessaire de recompiler le module,
ainsi que tous les modules utilisant le module modifié. Une fois que
tous les fichiers objets (les fichiers *.o
) concernés ont
été regénérés, il faut refaire l’édition de liens.Lorsque le nombre de module devient élevé, il devient difficile de
savoir quel(s) module(s) recompiler. On automatise alors la chaîne de
compilation, en utilisant l’outil make
.
Puisque la compilation se fait en 3 phases, 3 types d’erreurs peuvent survenir: * une erreur du préprocesseur (ie. une macro est mal écrite):
$ gcc -c foo.c
foo.c:1:8: error: no macro name given in #define directive
#define
^
$ gcc -c erreur_compil.c
erreur_compil.c: In function ‘f’:
erreur_compil.c:3:1: error: expected ‘;’ before ‘}’ token
}
^
$ gcc -o plip main.o
main.o : Dans la fonction « main » :
main.c:(.text+0x15) : référence indéfinie vers « mem_init »
collect2: error: ld returned 1 exit status
Les fichiers objets et exécutables sont sous le format ELF (Executable and Linkable Format)
Ensemble de sections regroupant les symboles d’un même type:
.text
contient les fonctions de l’objet.data
et .bss
contiennent les données
initialisées (.data
) ou pas (.bss
).symtab
contient la table des symbolesLors de l’édition de liens ou du chargement en mémoire, les sections de tous les objets sont fusionnés
La liste complète des sections du format ELF est disponible dans la
documentation (man 5 elf
).
La table des symboles contient la liste des
fonctions/variables globales (ou statiques) définies ou utilisées dans
le fichier. L’outil nm
permet de consulter cette table. Par
exemple:
$ nm mem_alloc.o
0000000000000000 C m
0000000000000007 T mem_allocate
0000000000000012 T mem_free
0000000000000000 T mem_init
$ nm main.o
0000000000000000 T main
U mem_init
Pour chaque symbole, nm
affiche l’adresse (au sein d’une
section), le type (donc, la section ELF), et le nom du symbole.
Ces informations sont également disponible via la commande
readelf
:
$ readelf -s mem_alloc.o
Table de symboles « .symtab » contient 12 entrées :
Num: Valeur Tail Type Lien Vis Ndx Nom
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS mem_alloc.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
8: 0000000000000001 0 OBJECT GLOBAL DEFAULT COM m
9: 0000000000000000 7 FUNC GLOBAL DEFAULT 1 mem_init
10: 0000000000000007 11 FUNC GLOBAL DEFAULT 1 mem_allocate
11: 0000000000000012 14 FUNC GLOBAL DEFAULT 1 mem_free
L’utilitaire objdump
permet lui aussi d’examiner la
table des symboles:
$ objdump -t mem_alloc.o
mem_alloc.o: format de fichier elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 mem_alloc.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 O *COM* 0000000000000001 m
0000000000000000 g F .text 0000000000000007 mem_init
0000000000000007 g F .text 000000000000000b mem_allocate
0000000000000012 g F .text 000000000000000e mem_free
Une variable déclarée dans une fonction peut être
int var;
ou int var2 = 17;
static
static int var = 0;
Puisqu’une variable locale statique est allouée au chargement du programme, elle apparaît dans la liste des symboles :
$ nm plop.o
0000000000000000 T function
0000000000000000 d variable_locale_static.1764
Ici, le symbole variable_locale_static.1764
correspond à
la variable variable_locale_static
déclarée
static
dans la fonction function
. Le suffixe
.1764
permet de différencier les variables nommées
variable_locale_static
déclarées dans des fonctions
différentes.
Une variable déclarée dans le fichier fic.c
en dehors
d’une fonction peut être:
int var;
ou int var2 = 17;
extern
extern int var;
fic.c
static
static int var = 0;
Les variables globales (déclarées extern
,
static
, ou “normales”) se retrouvent dans la table des
symboles de l’objet, mais dans des sections ELF différentes :
$ nm plop.o
0000000000000000 T function
U var_extern
0000000000000000 D var_globale
0000000000000004 d var_static_globale
La variable var_extern
(déclarée avec
extern int var_extern;
) est marquée "U"
(undefined). Il s’agit donc d’une référence à un symbole
présent dans un autre objet.
La variable var_globale
(déclarée avec
int var_globale = 12;
) est marquée "D"
(The symbol is in the initialized data section). Il s’agit donc
d’une variable globale initialisée .
La variable var_static_globale
(déclarée avec
static int var_static_globale = 7;
) est marquée
“d
” (The symbol is in the initialized data
section). Il s’agit donc d’une variable globale “interne”. Il n’est
donc pas possible d’accèder à cette variable depuis un autre objet:
$ gcc plop.o plip.o -o executable
plip.o : Dans la fonction « main » :
plip.c:(.text+0xa) : référence indéfinie vers « var_static_globale »
collect2: error: ld returned 1 exit status
#include "mem_alloc.h"
, puis
utilisation des fonctions-l
, par
exemple: -lmemory
libmemory.so
ou
libmemory.a
Avantages/inconvénients des bibliothèques statiques:
- Taille de l’exécutable important (puisqu’il inclut la bibliothèque);
- En cas de nouvelle version d’une bibliothèque (qui corrige un bug par exemple), il faut recompiler toutes les applications utilisant la bibliothèque;
- Duplication du code en mémoire;
+ L’exécutable incluant une bibliothèque statique fonctionne “tout seul” pas besoin d’autres fichiers).
Avantages/inconvénients des bibliothèques dynamiques:
+ Taille de l’exécutable réduite (puisqu’il n’inclut qu’une référence à la bibliothèque);
+ En cas de nouvelle version d’une bibliothèque (qui corrige un bug par exemple), pas besoin de recompiler les applications utilisant la bibliothèque;
+ Une instance du code en mémoire est partageable par plusieurs processus;
- L’exécutable incluant une bibliothèque dynamique ne fonctionne pas “tout seul”: il faut trouver toutes les bibliothèques dynamiques nécessaires.
Les “dépendances” dues aux bibliothèques dynamiques sont visibles
avec ldd
:
$ ldd executable
linux-vdso.so.1 (0x00007fff9fdf6000)
libmem_alloc.so (0x00007fb97cb9f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb97c7ca000)
/lib64/ld-linux-x86-64.so.2 (0x0000555763a9b000)
Il existe 2 types de bibliothèques
libmemory.**a**
ar rcs libmemory.a mem_alloc.o mem_plip.o mem_plop.o [...]
libmemory.**so**
gcc -shared -o libmemory.so mem_alloc.o mem_plip.o mem_plop.o [...]
-fPIC
:gcc -c mem_alloc.c -fPIC
Pour exécuter un programme utilisant une bibliothèque dynamique, le
système doit charger en mémoire le programme ainsi que la biblbiothèque.
Si la bibliothèque n’est pas installée dans un répertoire standard
(typiquement dans /usr/lib
), il peut être nécessaire
d’indiquer où trouver cette bibliothèque grâce à la variable
d’environnement LD_LIBRARY_PATH
.
$ ./mon_programme
mon_programme: error while loading shared libraries: libtruc.so: cannot open shared object file: No such file or directory
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/trahay/libs/libtruc/
$ ./mon_programme
It works !
L’avantage d’une bibliothèque statique est qu’il n’est nécessaire de connaître son emplacement qu’au moment d’édition de liens. Une fois l’exécutable crée, celui-ci inclue la bibliothèque statique. On peut donc déplacer l’exécutable, supprimer la bibliothèque statique, recopier l’exécutable sur une autre machine sans empêcher son exécution.
Toutefois, lorsqu’une bibliothèque statique est mise à jour (par exemple pour corriger un bug ou une faille de sécutité), il est nécessaire de recompiler tous les programmes utilisant cette bibliothèque. Pour des bibliothèques très utilisées (par exemple, la libc), cela peut être long et on risque d’oublier de recompiler certains programmes.
A l’inverse, si une bibliothèque dynamique est mise à jour, cette mise à jour est directement disponible pour toutes les applications utilisant la bibliothèque. Ceci explique pourquoi la plupart des distributions Linux reposent aujourd’hui sur des bibliothèques dynamiques plutot que statiques.
Attention, si une bibliothèque dynamique est mise à jour et que son ABI (Application Binary Interface) change (par exemple si la signature d’une fonction change), il sera nécessaire de recompiler les applications utilisant cette biblliothèque.
Organisation classique d’un projet:
src/
module1/
module1.c
module1.h
module1_plop.c
module2/
module2.c
module2.h
module2_plip.c
doc/
manual.tex
Besoin d’utiliser des flags
:
-Idir
indique où chercher des fichiers
.h
gcc -c main.c -I../memory/
-Ldir
indique à l’éditeur de lien où trouver des
bibliothèquesgcc -o executable main.o -L../memory/ -lmem_alloc
A l’exécution:
LD_LIBRARY_PATH
contient les répertoires où
chercher les fichiers .so
export LD_LIBRARY_PATH=.:../memory
Par défaut, le compilateur va chercher les fichiers d’entête dans un
certain nombre de répertoires. Par exemple, gcc
cherche
dans:
/usr/local/include
<libdir>/gcc/<target>/version/include
/usr/<target>/include
/usr/include
L’option -I
ajoute un répertoire à la liste des
répertoires à consulter. Vous pouvez donc utiliser plusieurs fois
l’option -I
dans une seule commande. Par exemple:
gcc -c main.o -Imemory/ -Itools/ -I../plop/
De même, l’éditeur de liens va chercher les bibliothèques dans un
certain nombre de répertoires par défaut. La liste des répertoires
parcourus par défaut par ld
(l’éditeur de lien utilisé par
gcc
) est dans le fichier /etc/ld.so.conf
. On y
trouve généralement (entre autre):
/lib/<target>
/usr/lib/<target>
/usr/local/lib
/usr/lib
/lib32
/usr/lib32
Si la variable LD_LIBRARY_PATH
est mal positionnée, vous
risquez de tomber sur ce type d’erreur au lancement de
l’application:
$ ./executable
./executable: error while loading shared libraries: libmem_alloc.so: cannot open \
shared object file: No such file or directory
executable
, j’ai besoin de
mem_alloc.o
et main.o
”executable
, il faut lancer la commande
gcc -o executable mem_alloc.o main.o
”Makefile
, ensemble de règles
sur deux lignes:cible : dependance1 dependance2 ... dependanceN`
<TAB>commande
make
make
make
parcourt le fichier
Makefile
du répertoire courant et tente de produire la
première cible.make cible
.make
s’arrêteMakefile
et des
date/heure de dernière modification des fichiers: si un fichier de
l’arbre est plus récent que la cible à générer, tout le chemin entre le
fichier modifié et la cible est regénéré.Makefile
Voici un exemple de fichier Makefile
:
: executable
all
: mem_alloc.o main.o
executable-o executable main.o mem_alloc.o
gcc
.o: mem_alloc.c mem_alloc.h
mem_alloc-c mem_alloc.c
gcc
.o: main.c mem_alloc.h
main-c main.c
gcc
emacs
,
lorsque vous éditez un fichier nommé Makefile
, les
tabulations sont colorisées en rose par défaut.Si vous utilisez des espaces à la place de la tabulation, la commande
make
affiche le message d’erreur suivant:
Makefile:10: *** missing separator (did you mean TAB instead of 8 spaces?). Arrêt.
make
(sans argument) génère la
première cible, on ajoute généralement une premier cible “artificielle”
all
décrivant l’ensemble des exécutables à générer:all: executable1 executable2
Dans ce cas, seule la cible est spécifiée. Il n’y a pas d’action à effectuer.
clean
clean
pour “faire le ménage” (supprimer les exécutables et
les fichiers .o
):clean:
<TAB>rm -f executable1 executable2 *.o
Makefile
, on peut utiliser
certaines notations symboliques:
$@
désigne la cible de la règle$^
désigne l’ensemble des dépendances$<
désigne la première dépendance de la liste$?
désigne l’ensemble des dépendances plus récentes que
la cibleMakefile
. Par exemple:=executable
BIN=mem_alloc.o main.o
OBJETS=-Wall -Werror -g
CFLAGS=-lm
LDFLAGS
: $(BIN)
all
: $(OBJETS)
executable-o executable main.o mem_alloc.o $(LDFLAGS)
gcc
.o: mem_alloc.c mem_alloc.h
mem_alloc-c mem_alloc.c $(CFLAGS)
gcc
.o: main.c mem_alloc.h
main-c main.c $(CFLAGS)
gcc
:
clean-f $(BIN) $(OBJETS) rm
make
n’est qu’une partie
de la chaîne de configuration et de compilation.autoconf
/automake
ou
Cmake
sont fréquemment utilisés pour écrire des
“proto-makefiles”