Participer au site avec un Tip
Rechercher
 

Améliorations / Corrections

Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.

Emplacement :

Description des améliorations :

Compilation d'un programme C

Introduction et historique Eclipse/CDT


Accès rapide :
Les différents compilateurs C
gcc (Gnu C Compiler)
MinGW (MINimalist Gnu for Windows)
Clang
Visual C++
Installation de votre compilateur
Installation de GCC
Installation de MinGW
Le processus de compilation
Utilisation de gcc pour vos compilations
Lancement du préprocesseur
Lancement de la compilation
Lancement de l'édition des liens
Utilisation de l'outil make
Ecriture d'un fichier makefile

Les différents compilateurs C

Quand on parle de langages de programmations, on peut souvent les classer selon différents critères. Par rapport à notre débat actuel, on peut opposer les langages de programmation interprétés et les langages compilés. Un langage interprété (Javascript, basic, ...) présuppose qu'un environnement d'exécution soit installé sur le poste pour interpréter les différentes instructions du langage et les exécuter : ces langages ne permettent pas de produire des exécutables très efficace (efficace certes, mais pas les plus efficaces). Au contraire, les langages compilés (C, C++, ...) garantissent les meilleurs performances étant donné que le code source est traduit une fois pour toute (avant la première exécution) en langage machine. Pour rappel, le langage machine est le seul langage qui puisse être directement exécuté par votre processeur. Ainsi, les langages compilés évitent les étapes de traduction du code durant l'exécution (l'interprétation).

Un compilateur est un outil qui permet donc la transformation d'un code source en langage machine. Le langage C nécessite donc un compilateur pour traduire, une fois pour toute, votre code source C. Il existe plusieurs compilateurs C, en fonction du système d'exploitation utilisé et des sociétés d'édition logiciel retenues. Voici une description de quelques compilateurs C.

gcc (Gnu C Compiler)

En premier lieu, il est important de noter que le terme gcc est ambigu : il est associé à deux éléments.

Ces deux éléments sont proposés par la Free Foundation Software (GNU). Le site officiel GCC est disponible à l'adresse Internet suivante : http://www.gnu.org/software/gcc/.

Le compilateur gcc est développé sur les différentes plates-formes Linux/Unix. Il est connu pour relativement bien supporter les différentes versions du standard ISO du langage C. Il produit des exécutables efficaces. Il est considéré comme une référence en termes de compilateur C. De plus, il permet de faire de la « cross-compilation » : cela permet de travailler sur une plate-forme et d'y produire un exécutable pour une autre plate-forme.

Remarque : c'est ce compilateur que nous considérerons pour la compilation des exemples de code de ce tutorial.

MinGW (MINimalist Gnu for Windows)

Il s'agit d'un portage du compilateur gcc pour la plate-forme Microsoft Windows. Le site officiel MinGW est disponible à l'adresse Internet suivante : http://www.mingw.org/.

L'ensemble des options proposées par MinGW restent les mêmes que celles de gcc. Il en résulte que les informations proposées tout au long de ce tutorial pour compiler vos programmes s'appliquent donc parfaitement à MinGW.

Clang

Ce compilateur est une alternative à gcc. Le site officiel de Clang est disponible à l'adresse Internet suivante : http://clang.llvm.org. Il est notamment utilisé sur les plates-formes Mac, mais peut aussi être utilisé sur d'autres plates-formes. Il permet aussi la cross-compilation.

Visual C++

Le terme « Visual C++ » est utilisé pour qualifier l'ensemble des outils liés au développement C++ sous l'environnement Visual Studio de la société Microsoft. Dans cet ensemble d'outils, vous trouverez notamment le compilateur C++ de la société Microsoft. Ce dernier est très performant et supporte relativement bien les derniers standards du langage C. Sur environnement Windows, il est donc en concurrence avec le compilateur MinGW.

Installation de votre compilateur

Ne pouvant pas être exhaustif sur toutes les procédures d'installation de tous les compilateurs sur toutes les plates-formes disponibles, nous allons, dans ce tutorial, nous limiter à l'installation de gcc sur plate-forme Linux et de MinGW sur plate-forme Windows.

Installation de GCC

Normalement un système d'exploitation Linux/Unix permet l'installation des logiciels via un gestionnaire de paquets logiciels. La procédure d'installation est donc dépendante de la distribution utilisée. Mais il y aura toujours deux manières de procéder : soit vous utiliser l'interface graphique de votre gestionnaire de paquets, soit vous procédez par lignes de commande (c'est cette approche que nous présentons ici).

Si vous utilisez une distribution Linux Fedora (ou compatible), la procédure à suivre est la suivante :

$> sudo yum install gcc

Si vous êtes utilisateur d'une distribution Ubuntu (ou compatible), la procédure d'installation est la suivante :

$> sudo aptget install gcc

Une fois l'installation terminée, vérifiez le bon fonctionnement de votre compilateur en exécutant la commande suivante et vérifiez qu'un message demandant de fournir un fichier de code source C vous soit bien présenté.

$> gcc
gcc: fatal error: no input files
compilation terminated.
$>

Installation de MinGW

Pour installer MinGW, rendez-vous sur le site officiel (http://www.mingw.org/) puis cliquez sur « Download ». Vous devriez pouvoir localiser l'installeur. En fait, il s'agit juste d'une amorce permettant de choisir les modules MinGW à installer sur votre machine. Commencer par indiquer le répertoire d'installation.

Et maintenant, sélectionnez les éléments à installer suivants :

Enfin, lancez l'installation des modules sélectionnés.

Une fois l'installation terminée, vérifiez le bon fonctionnement de votre compilateur en exécutant la commande suivante et vérifiez qu'un message demandant de fournir un fichier de code source C vous soit bien présenté.

C:\> gcc
gcc: fatal error: no input files
compilation terminated.
C:\>

Le processus de compilation

Le processus de construction d'un exécutable à partir de fichiers de code C n'est pas si simple qu'il n'y parait. Afin de mieux comprendre les subtilités de ce processus, commençons par le présenter et comprendre ses différentes étapes. Effectivement, il y a trois étapes distinctes dans ce processus de construction : la phase de preprocessing, la phase de compilation à proprement parler et la phase d'édition des liens. Voici un diagramme présentant la succession de ces trois étapes.

Il faut comprendre qu'un programme C peut être très grand et donc peut nécessiter un temps de compilation relativement long (pourquoi pas plusieurs heures). Si pour une petite modification portant sur quelques lignes, il faut relancer une compilation intégrale d'un tel programme, la mise au point de celui-ci deviendrait très compliquée et longue. C'est pour cette raison que le processus de compilation présenté ci-dessus a été retenu. Effectivement. Si un seul fichier de code source C est modifié, il faudra simplement recompiler ce fichier et rejouer la phase d'édition des liens.

Pour autant, la compilation d'un fichier se passe en deux temps : en premier lieu, le préprocesseur C est invoqué, ce qui produit un fichier temporaire en mémoire, puis ensuite, la phase de compilation est lancée sur ce fichier temporaire. Le préprocesseur est un outil de prétraitement de votre fichier de code source : nous reviendrons sur l'intérêt de cette étape dans un prochain chapitre. Comme à la fin de la phase de préprocesseur, aucun fichier n'est produit sur le disque dur, les développeurs C ont tendance à regrouper les deux premières phases du processus (préprocesseur et compilation) sur le terme générique de compilation. Au terme de la compilation, un fichier d'extension .o (.obj sous Windows) est produit : il contient le code machine équivalent à votre programme.

La phase d'édition des liens permet de rassembler tous les fichiers .o pour produire l'exécutable. Cette étape est requise, car comme le programme est réparti sur différents fichiers, quand vous en compilez un seul, les appels aux fonctions définies dans d'autres fichiers de codes ne peuvent être correctement liés. La phase d'édition des liens interconnecte donc tous les appels de fonctions inter-fichiers. Seul l'exécutable finalement produit peut être exécuté (un .o ne pouvant pas correctement fonctionner).

Notez la présence sur le diagramme du module CRT (C RunTime) : il contient l'ensemble des fonctions prédéfinies dans la librairie standard du langage C. Il faudra lui aussi le lier au reste de votre programme.

Utilisation de gcc pour vos compilations

Tout d'abord, notez que gcc est un outil de compilation qui se lance en mode « Ligne de commande ». Commençons donc avec ce premier exemple : il vous permettra d'obtenir l'aide en ligne et la liste des options que gcc met à votre disposition.

$> gcc --help
Usage: gcc [options] file...
Options:
  -pass-exit-codes         Exit with highest error code from a phase
  --help                   Display this information
  --target-help            Display target specific command line options
  --help={common|optimizers|params|target|warnings|[^]{joined|separate|undocumented}}[,...]
                           Display specific types of command line options
  (Use '-v --help' to display command line options of sub-processes)
  --version                Display compiler version information
...
...
$> 

Il est clair que nous n'allons pas dans ce chapitre voir 100% des options proposées. Néanmoins nous allons nous concentrer sur certaines d'entre elles. Afin de pouvoir travailler, nous allons considérer le programme C suivant : il cherche notamment à définir un calcul d'un minimum. Afin de tester toutes les phases du processus de compilation, nous définissons le calcul du minimum sous forme d'une macro (correspondant à la ligne #define MINI( a, b ) a<b ? a : b). Un macro pourrait ressembler à une fonction, sauf que ce n'est pas le cas du tout. Une macro est substituée par le préprocesseur par son code équivalent, et ce avant la phase de compilation, alors qu'une fonction est prise en charge par le compilateur. Ne pas tenir compte de cette différence peut vous amener à produire quelques bugs sympathiques. C'est le cas du programme suivant. Question : à votre avis, quelle sera la valeur de la variable result ?

#include <stdio.h>
#include <stdlib.h>

#define MINI( a, b ) a<b ? a : b

int main() {

    int a = 3;
    int b = 6;
    int c = 33;

    int result = MINI( a, b ) + 2;
    printf( "result == %d \n", result );   

    return EXIT_SUCCESS;
}

Si a (une variable de type entier) vaut 3 et b (idem) vaut 6, alors normalement, le minimum de a et b devrait valoir 3 auquel on devrait ajouter 2. A la question précédemment posée, vous devriez, peut-être, avoir répondu 5. Nous allons vérifier en lançant une compilation totale (préprocessing, compilation et édition des liens) de votre programme. Je pars du principe que vous avez codé ce programme dans un fichier nommé Sample.c.

$> gcc -o Sample Sample.c
$> ./Sample
result == 3 
$>

L'option -o permet de demander au compilateur de directement produire l'exécutable Sample à partir du fichier de code source Sample.c. Ensuite nous lançons l'exécution de l'exécutable produit. Sauf que ... le programme devait répondre 5 et non 3. Pourquoi ?

Lancement du préprocesseur

Afin de répondre à la question précédente, nous allons utiliser une autre option de gcc : l'option -E. Elle permet d'interrompre la construction de votre programme juste après la phase de préprocessing : cela vous permettra de voir le résultat produit par ce dernier. Pour information, les instructions du préprocesseur sont faciles à reconnaitre : elle sont toutes préfixées du caractère #. Le résultat sera produit dans un fichier Sample.temp via l'option -o.

$> gcc -o Sample.temp -E Sample.c 

Si vous regardez le fichier produit, vous verrez que les deux premières lignes du l'exemple initial (les deux lignes commençant par #include) ont été substituées par le contenu de ces deux fichiers. Il faut même notez que ces deux fichiers faisaient eux-mêmes des #include qui ont aussi étaient récursivement inclus. Du coup, le fichier produit est bien plus volumineux que l'original.

Autre constat, la macro a bien été substituée : vous pouvez vérifier cette affirmation en regardant à la fin du fichier produit. Vous devriez y retrouver la ligne de code C suivante : int result = a<b ? a : b + 2;. Ce qui s'est passé, c'est que le code de la macro a été injecté en lieu et place de l'utilisation de cette dernière. Or l'opérateur conditionnel (condition ? codeSiVrai : codeSiFaux) est moins prioritaire que l'opérateur + d'addition. Le +2 ne s'appliquera donc que dans le cas où la valeur minimale est passée en second paramètre, d'où le résultat du programme. Nous en déduisons que la définition de la macro doit plutôt être :

#define MINI( a, b ) ((a)<(b) ? (a) : (b))

Après substitution de la macro nous auront maintenant : int result = ((a)<(b) ? (a) : b) + 2;. Cela sera beaucoup mieux. Ce que j'ai voulu montrer au travers de cet exemple, c'est qui est parfois utile de voir le code produit par le préprocesseur : si on ne le demande pas explicitement, on ne sera pas forcément conscient de tout ce qui pourra se passer dans le programme.

Lancement de la compilation

Il est maintenant temps de compiler votre fichier de code C, mais nous choisissons de nous arrêter juste après la phase de compilation (sans exécuter la phase d'édition des liens). Pour ce faire, il faut utiliser l'option -c. Notez que dans l'exemple ci-dessous nous lançons gcc sur le fichier .c : la phase de préprocessing est donc bien exécutée mais sans générer explicitement le fichier temporaire. Cette phase est donc directement suivie de la phase de compilation. Au final, un fichier d'extension .o est produit.

$> gcc -c Sample.c
$> ls Sample.*
Sample.c Sample.o
$>

Notez que dans l'exemple proposé, tout ce passe bien. Néanmoins, il n'en sera pas toujours ainsi. Deux types de problèmes peuvent être rencontrés. Les erreurs de compilation surviennent quand le compilateur est incapable de produire le code machine associé à votre programme (elles peuvent être de différentes natures : erreurs de syntaxe, erreurs de typage, ...). Des avertissements (warnings en anglais) peuvent aussi être affichés quand des incohérences sont repérées, mais que la génération d'un exécutable peut quand même aboutir.

Essayons de produire une erreur de compilation. Par exemple, supprimer le point-virgule à la fin de la ligne débutant par printf. Voici le message qui sera alors généré par votre compilateur.

$> gcc -c Sample.c
Sample.c: In function 'main':
Sample.c:15:5: error: expected ';' before 'return'
     return EXIT_SUCCESS;
     ^
$>

Remettez, svp, le point-virgule en fin de ligne. Si vous souhaitez voir apparaître un warning, il faut savoir qu'il existe plusieurs niveaux de warnings. Si vous ajoutez l'option -Wall vous demandez alors au compilateur de produire tous les niveaux de warnings. Dans l'exemple proposé ci-dessus, je ne sais pas si vous avez remarqué, mais la variable c ne sert à rien. Voici ce que produit le compilateur avec l'option proposée.

$> gcc -c -Wall Sample.c
Sample.c: In function 'main':
Sample.c:10:9: warning: unused variable 'c' [-Wunused-variable]
     int c = 33;
         ^
$>

La logique des choses voudrait que vous utilisiez systématiquement l'option -Wall et qu'en fin de compilation, aucun avertissement et aucune erreur ne soit produit.

Vous pouvez aussi demander au compilateur de réaliser un maximum d'optimisation sur le code machine produit. Pour ce faire il existe plusieurs options correspondant à plusieurs niveaux d'optimisations à réaliser. Voici quatre option associées à ce sujet : -O0 (pas d'optimisation), -O1, -O2 et -O3 (le plus haut niveau d'optimisation).

$> gcc -c -O3 -Wall Sample.c

Lancement de l'édition des liens

Pour générer l'exécutable à partir de notre fichier .o et des librairies standards (CRT), vous pouvez lancer la commande suivante.

$> gcc -o Sample Sample.o

Imaginons un autre exemple de code qui utiliserait deux fichiers de code C (file1.c et file2.c). Voici la liste des étapes de construction de votre programme. Comprenez que si un seul fichier de code source est modifié alors une partie du code ne sera pas à recompilé. Une telle approche permet de réduire de temps de construction des gros programmes.

$> gcc -c -O3 -Wall file1.c
$> gcc -c -O3 -Wall file2.c
$> gcc -o executable file1.o file2.o

Rappel des quelques options proposées dans ce document :

-E    : pour demander l'arrêt du processus de construction
        de l'exécutable après la phase de préprocessing.
-c    : pour demander l'arrêt du processus après la phase de compilation.
-o    : pour spécifier le nom du fichier produit en sortie (output).
-Wall : pour afficher un maximum de message d'avertissement (warnings).
-03   : pour demander un maximum d'optimisations sur le code machine produit.

Utilisation de l'outil make

Un autre outil, très importante pour le langage C, rentre en jeux dans la chaîne d'outils utilisée pour la construction de vos programmes (on parle d'outil de build) : l'outil make. Cet outil permet concentrer toutes les lignes de commande nécessaire à la compilation en un seul fichier appelé makefile. Un aspect important de l'outil make et qu'une ligne de commande ne sera lancée que si elle est nécessaire. Ainsi, si un fichier de code source n'a pas été modifié depuis la dernière compilation, alors le .o précédemment généré sera conservé. Si, au contraire, le fichier a été modifié alors une régénération sera lancée. C'est la date de dernière modification des fichiers (sur le disque) qui permettra de déterminer si la cible à construire est à jour ou non.

Notez que l'outil make est livré en standard avec la suite de compilateurs GCC et avec MinGW. Dans ces deux cas, vous n'avez donc pas d'installation supplémentaire à effectuer.

Ecriture d'un fichier makefile

Un fichier makefile est notamment constitué de règles. Une règle commence par le nom de la cible à construire suivi d'un caractère :, puis des dépendances permettant de la construire. Cette règle sera à exécuter si la date de dernière modification de la cible est plus récente que celle de l'une des dépendances. Si c'est le cas, les lignes de commande servant à construire la cible seront exécutées. On reconnait ces lignes de commandes car elles sont situées sous la ligne de définition de la cible et commencent toutes par un retrait d'une tabulation. Voici la syntaxe générale d'une règle de construction.

target: dependance1 dependance2
    commandLine

Considérons un programme C basé sur deux fichiers de code source (file1.c et file2.c). Voici alors un exemple possible de fichier makefile.

all: Sample
    @echo "Compilation terminated"

clean:
    rm file1.o file2.o Sample

Sample: file1.o file2.o
    gcc -o Sample file1.o file2.o

file1.o: file1.c
    gcc -c -Wall -O3 file1.c

file2.o: file2.c
    gcc -c -Wall -O3 file2.c

Analysons ce fichier de construction. On y trouve cinq cibles à construire : all, clean, Sample, file1.o et file2.o. La première cible du fichier sera lancée implicitement lors de l'exécution de la commande make sans y passer d'argument. On place fréquemment une cible all dans un fichier makefile car il est possible d'y générer plusieurs exécutables. Dans ce cas, tous les exécutables à produire sont placés en dépendances de all. On trouve aussi frequemment une cible clean : elle permet de supprimer tous les fichiers pouvant être régénérés. A la fin de l'exécution de la cible all, seuls les codes sources continus à exister.

Pour ce qui est trois dernières cibles, nous y retrouvons la construction du programme à proprement parler. Notez bien que les cibles sont liées les unes aux autres par leurs dépendances. Voici un petit exemple d'utilisation de l'outil make et de ce makefile.

$> make
gcc -c -Wall -O3 file1.c
gcc -c -Wall -O3 file2.c
gcc -o Sample file1.o file2.o
Compilation terminated
$> make clean
rm file1.o file2.o Sample
$> make all
gcc -c -Wall -O3 file1.c
gcc -c -Wall -O3 file2.c
gcc -o Sample file1.o file2.o
Compilation terminated
$> make clean
rm file1.o file2.o Sample
$> make file1.o
gcc -c -Wall -O3 file1.c
$> make
gcc -c -Wall -O3 file2.c
gcc -o Sample file1.o file2.o
Compilation terminated
$> touch file1.c
$> make
gcc -c -Wall -O3 file1.c
gcc -o Sample file1.o file2.o
Compilation terminated
$> make clean
rm file1.o file2.o Sample
$>

J'espère qu'au terme de ce chapitre, la construction d'un programme C est devenue plus claires à vos yeux. Qu'ils s'agissent du compilateur ou de l'outil make, il doit être clair qu'il existe de nombreuses autres possibilités. L'objectif de ce chapitre était simplement de vous mettre « le pied à l'étrier ». Je vous laisse rentrer plus sérieusement dans le sujet en consultant les documentations de ces deux produits.



Introduction et historique Eclipse/CDT