Compilation

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:

Pré-processing

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;
}
       
On peut affecter la valeur de macros a la compilation avec l'option -DMON_SYMBOL ou -DMON_SYMBOL=valeur. On utilisera généralement des noms de symboles en majuscules pour les différencier des simples variables.

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
       

Compilation

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.

Assemblage

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.

Edition de liens

L'édition de liens prend un ensemble de fichiers objets pour produire un programme exécutable.

Heureusement, dans la grande majorité des cas, on n'a pas à se préoccuper de tous ces fichiers intermédiaires, car ils sont gérés de façon transparente par le compilateur, à l'exception des fichiers objets. Ces fichiers et ces formats temporaires ne sont pas visibles à l'utilisateur sauf cas spécial. Mais il n'est pas inutile de savoir qu'ils existent.

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:

-c

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.

-Idirectory

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.

-o executable

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:

-g

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.

-On

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.

-DSYMBOL[=valeur]

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

-Ldirectory -llibrary

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.