La compilation d'un programme est un ensemble de traitements successifs sur un fichier source dont l'étape ultime est de générer un fichier exécutable par la machine. Les traitements successivement effectués sur le fichier source sont les suivants:
Le pré-processing consiste
à substituer toutes les macros présentes dans le code par leur
valeur: #ifdef
, #define
,
#include
, etc.
Voici un petit exemple de code sur lequel le préprocesseur
va fonctionner:
/* * Exemple d'utilisation de macros */ #include <stdio.h> #define BORNE_MAX 100 #define MAX(a,b) ((a)>(b)?(a):(b)) int table [BORNE_MAX]; int main() { int i; for (i=0; i < BORNE_MAX; i++) table [i]=i; #ifdef DEBUG printf ("DEBUG : table[10]=%d\n", table[10]); #endif return 0; } |
Les personnes de nature curieuse pourront examiner le code obtenu à l'issue du pré-processing avec l'option -E du compilateur:
% gcc -E fichier.c > fichier.E |
L'étape de compilation par elle-même. Elle travaille sur le fichier résultat du préprocessing, et produit un fichier texte contenu du code en langage d'assemblage spécifique à la machine sur laquelle vous compilez.
L'étape d'assemblage prend le fichier précédent, et génère du code machine. Le fichier produit est appelé fichier objet, et se reconnait en général par son extension .o.
L'édition de liens prend un ensemble de fichiers objets pour produire un programme exécutable.
Examinons un petit programme d'exemple:
/* * main.c */ #include <stdio.h> #include <file1.h> int main() { printf ("%d\n",file1_proc (10)); return 0; } |
/* * file1.h */ extern int file1_proc (int i); |
/* * file1.c */ int file1_proc (int i) { return i+1; } |
% gcc -c file1.c % gcc -I. -c main.c % gcc -o exemple main.o file1.o % exemple 11 % gcc -v -c file1.c Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/specs gcc version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release) /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/cpp -lang-c -v -undef -D__GNUC__=2 -D__GNUC_MINOR__=91 -D__ELF__ -Dunix -Di386 -D__i386__ -Dlinux -D__ELF__ -D__unix__ -D__i386__ -D__i386__ -D__linux__ -D__unix -D__i386 -D__linux -Asystem(posix) -Asystem(unix) -Acpu(i386) -Amachine(i386) -Di386 -D__i386 -D__i386__ -D__tune_i386__ file1.c /tmp/cc2jRiec.i GNU CPP version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release) (i386 Linux/ELF) #include "..." search starts here: #include <...> search starts here: /usr/local/include /usr/i386-redhat-linux/include /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/include /usr/include End of search list. /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/cc1 /tmp/cc2jRiec.i -quiet -dumpbase file1.c -version -o /tmp/cc2F6SSd.s GNU C version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release) (i386-redhat-linux) compiled by GNU C version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release). as -V -Qy -o file1.o /tmp/cc2F6SSd.s GNU assembler version 2.9.1 (i386-redhat-linux), using BFD version 2.9.1.0.24 % gcc -v -o exemple main.o file1.o Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/specs gcc version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release) /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/collect2 -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o exemple /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/crtbegin.o -L/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66 -L/usr/i386-redhat-linux/lib main.o file1.o -lgcc -lc -lgcc /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/crtend.o /usr/lib/crtn.o |
Le compilateur C est appelé par la commande cc ou gcc. Son fonctionnement peut être modifié par une impressionnante floppée d'options sur la ligne de commande. Pour avoir une aide exhaustive sur un compilateur donné, il est recommandé de se reporter au manuel en ligne (accessible par la commande man gcc). Les options essentielles sont les suivantes:
Cette option indique au compilateur de s'arrêter après la génération du fichier objet (de ne pas faire l'édition de liens). Elle permet la compilation séparée.
Cette option indique le chemin
de recherche pour trouver les fichiers
inclus dans les macros #include <fichier.h>
.
Un certain nombre de répertoires sont prédéfinis et n'ont pas
besoin d'être spécifiés. L'ordre des répertoires de recherche
est important. Une variante existe: les macros
#include "fichier.h"
se limitent au répertoire
courant.
Cette option indique le nom de l'exécutable qui sera généré à l'édition de liens. Par défaut, si on ne précise rien, l'exécutable est appelé a.out en référence au format dans lequel est écrit ce fichier.
Note: Le format a.out a disparu de la circulation au profit du format ELF, plus portable et plus extensible.
Toute liste de fichiers sera considérée comme des fichiers devant être traités dans le processus de compilation. Le traitement à leur appliquer dépendra de leur suffixe. Par exemple gcc file1.c file2.o va compiler file1.c, générer temporairement file1.o, lier ensuite file1.o et file2.o dans un exécutable nommé a.out.
D'autres options de compilation peuvent s'avérer utiles:
Cette option rajoute des informations supplémentaires dans chacun des fichiers produits pour faciliter la tache de debuggage. Cela permet en utilisant un debugger (par exemple gdb) d'avoir accès aux noms de variables utilisées dans le source, de savoir pour chaque instruction machine exécutée à quel fichier source, et à quelle ligne de code cela correspond. La taille des fichiers ainsi générés augmente sensiblement, on évitera donc de laisser trainer ces options en cycle de production ou lorsque l'on fournit un produit fini.
Cette option indique si
l'on souhaite produire un code optimisé. La valeur n
indique le niveau d'optimisation souhaité, de zéro (pas
d'optimisation, jusqu'à 6 ou plus, ceci dépend du compilateur).
Chaque niveau correspond à la sélection d'un groupe d'options
d'optimisation indépendantes. On peut choisir une optimisation
specifique en précisant l'option
-foptimisation_specifique ou
au contraire pour interdire une optimisation spécifique par
-fno_optimisation_specifique.
Chaque option d'optimisation est décrite abondamment dans les pages
du manuel en ligne du compilateur. Il est possible de mixer les
options d'optimisation et de débugage (-g -O2
par exemple), mais cela n'est pas recommendé car cela rend le code
délicat à débugger. L'optimiseur se réserve le droit de
supprimer des variables inutiles, de précalculer des expressions,
de changer l'ordre d'exécution des instructions. Toutes ces
modifications perturbent l'utilisateur qui s'attend à avoir une
exécution séquentielle des lignes de son code source sous le
debugger.
Cette option permet de définir des constantes ayant une valeur pour le préprocesseur. L'exemple précédent montre que le compilateur ajoute lui-même un grand nombre de ces constantes, permettant de caractériser le système sur lequel la compilation s'exécute (compilation conditionnelle).
Ces options sont utilisées pour l'édition de liens. Elles permettent d'inclure des fichiers objets supplémentaires à ceux que l'on a compilé. Ces fichiers objets sont regroupés dans un fichier unique, nommé librairie. Une librairie est donc un ensemble de fichiers .o. Elle peut être liée statiquement ou dynamiquement à l'exécutable.
Dans le premier cas, le contenu de la librairie est ajouté dans le fichier exécutable une fois pour toute. Cela rend l'exécutable self-contained. Tout le code dont il a besoin pour fonctionner est inclus dans le fichier executable. Le désavantage de cette méthode est que la mise à jour de la librairie ne pourra se faire que par une recompilation de notre fichier source qui est à la charge de l'utilisateur.
Dans le second cas, l'exécutable ne contient qu'un lien
vers cette librairie. Il est donc de plus petite taille. Il n'y a
cependant pas de miracle, le code de la librairie devra pourtant
être chargé au moment de l'exécution (on parle de
runtime). Pour cela, une variable
d'environnement LD_LIBRARY_PATH
contient une liste
de répertoire dans laquelle le loader dynamique recherchera la
librairie souhaitée au lancement de l'exécutable. L'intérêt est
d'avoir des exécutables qui ne dupliquent pas le code d'une même
librairie, et de pouvoir mettre à jour une librairie sans avoir
besoin de recompiler les programmes qui l'utilisent (pour peu que
cette librairie continue d'utiliser la même API,
Application Programming Interface. Des numéros
de versions majeurs et mineurs sont utilisés pour
gérer la compatibilité entre les versions d'une librarie).
L'option -Ldirectory indique le directory où le linker doit rechercher la librairie (au moment de l'édition des liens), et l'option -llibrary precise le nom de cette librairie. Selon les architectures, le fichier aura comme nom liblibrary.so pour une librairie dynamique ou liblibrary.a pour une librairie statique.
Un petit exemple sur la création de librairies:
% gcc -c fichier1.c fichier2.c fichier3.c % ar cr libessai.a fichier1.o fichier2.o fichier3.o % ranlib libessai.a % gcc -c main.c % gcc -static -o executable main.o -L. -lessai % gcc -static -o executable main.o libessai.a |
Les deux dernières commandes sont équivalentes. Cet exemple illustre la création d'une librairie statique et son utilisation pour générer executable.
% gcc -c -fPIC fichier1.c fichier2.c fichier3.c % gcc -shared -o libessai.so fichier1.o fichier2.o fichier3.o % gcc -c main.c % gcc -o executable main.o -L. -lessai |
Cet exemple illustre la création d'une librairie dynamique et son utilisation pour générer executable.
Selon les cas, les systèmes Unix utilisés, et le type de compilateur utilisé, la syntaxe peut varier. Ces exemples s'appuient sur les outils GNU. Pour avoir les syntaxes à s'appliquant à un système praticulier, il faut se référer aux documentations, en particulier les pages de manuel de cc, et ld pour le linker dynamique.