Compilation avec GCC

Posté le dim 05 février 2017 dans devtools
Article de la série build : Compilation avec GCC - Écrire un Makefile

GCC, Gnu Compiler Collection, est l'ensemble des outils nécessaires pour transformer votre code source en un binaire exécutable. Un simple gcc hello.c appel une suite d'outils dans un ordre précis pour créer un binaire nommé a.out.

Les phases de compilation, petit rappel

Lorsque l'on compile un fichier c, gcc va appeler pour vous les outils suivants dans cet ordre :

  • Le préprocesseur (cpp): traite l'ensemble des directives de précompilation tel que #include, #define, #ifndef. Ici, il s'agit simplement de faire des copiés/collés dans votre code.

  • Le compilateur (cc1): vérifie la syntaxe, le respect des prototypes des fonctions et transforme votre code C en assembleur.

  • L'assembleur (as): transforme (assemble) le code assembleur en binaire compréhensible pour le micro-processeur. On nomme le résultat obtenu : fichier objet. Ce n'est toujours pas un exécutable.

  • Le linker (ld): nommé éditeur de liens en français, il va lier votre fichier objet avec les bibliothèques nécessaires (il faut le code de printf, de gets, ...) et également ajouter un loader qui permettra de charger l'application en mémoire.

Il est possible d'arrêter gcc à chacune de ces étapes :

  • gcc -E demo.c, donnera un fichier demo.i, résultat du préprocesseur.

  • gcc -S demo.c, donnera un fichier demo.s, qui contient le code assembleur générer par le compilateur.

  • gcc -c demo.c, donnera un fichier demo.o, qui est notre fichier objet (binaire non exécutable).

Options usuelles de gcc

Il existe un nombre incroyable d'options pour gcc, voici les plus couramment utilisées.

  • -o : pour output, elle permet de spécifier le nom de l'application. Sans cette option, votre programme se nommera a.out. gcc -o hello hello.c donnera un binaire nommé hello.

  • -g : ajoute les symboles (votre code en format txt) dans le binaire, cela permet d'utiliser un débogueur.

  • -D TRACE : est l'équivalent de #define TRACE. Cela permet d'activer du code placé entre #ifdef #endif sans devoir modifier vos fichiers pour y écrire (ou supprimer) #define TRACE.

  • -D SIZE_MAX=100 : est l'équivalent de #define SIZE_MAX 100.

  • -Wall : permet d'afficher un maximum de warning sur votre code.

  • -Wextra : ajout des vérifications supplémentaires sur le code.

  • -pedantics : demande à gcc de vérifier que votre code respecte la norme ISO C.

  • -std= : permet de préciser la norme C à utiliser. Les valeurs peuvent être :

    • c89, c90 : pour l'ISO C90
    • c99 : pour l'ISO C99
    • c11 : pour l'ISO C2011

Il existe également les extensions gnu : gnu90, gnu99, gnu11, gnu17. Si vous souhaitez compiler votre code avec d'autres compilateur que gcc, n'utilisez pas ces extensions.

  • -ansi : est équivalent à -std=c90, mais interdit les commentaires du type //.

  • -O2 : optimise la vitesse d'exécution de votre code. En bref, le compilateur remanie votre code pour le rendre plus rapide. Cette option est à utiliser uniquement lorsque votre programme est au point. Déboguer un programme compilé en -O2 est nettement moins facile.

  • -I include_path : permet d'indiquer un dossier où gcc peut trouver des fichiers .h.

  • -L lib_path : permet d'indiquer un dossier où le linker peut trouver des bibliothèques qui ne sont pas dans /usr/local/lib, /usr/lib ou dans /lib.

  • -l libname : permet d'indiquer au linker le nom d'une bibliothèque. Si vous utilisez une fonction mathématique comme sin ou ceil, son code se trouve dans le fichier libm.so. L'option à passer sera alors : -lm (le nom du fichier sans le lib et l'extension).

Exemple

Voici un petit code C qui utilise la libm.so ainsi qu'un ensemble de fichiers .h que nous aurions placé dans un dossier MyInclude à la racine du dossier utilisateur.

#include <stdio.h>
#include <math.h>
#include "myinclude1.h"
#include "myinclude2.h"
int main ()
{
  double a = 5.0;
  printf ("sin(%g) = %g\n",a,sin (a));
#ifdef MAXVAL
  printf ("code optionnel, MAX = %d\n",MAX);
#endif
  return 0;
}

Pour compiler ce fichier en mode debug : gcc -g -o myApp main.c -lm -I$HOME/MyInclude

Pour compiler ce fichier en mode release : gcc -O2 -o myApp main.c -lm -I$HOME/MyInclude

Pour activer le code optionnel, il faut juste ajouter -DMAXVAL aux commandes précédentes.

Le code optionnel reprend une variable MAX non définie. l'idée est de définir MAX par le biais de la ligne de commande : gcc -g -o myApp main.c -lm -I$HOME/MyInclude -DMAXVAL -DMAX=5

Importance du paramètre -std

Le langage C a été inventé pour être portable. L'idée est qu'il suffit de compiler le code pour le faire fonctionner sur un microprocesseur différent de celui où on là écrit. Il faut évidemment disposer d'un compilateur C pour ce µP.

Il arrive fréquemment d'écrire et tester du code sur PC et de le compiler ensuite pour une autre plateforme. Il ne faut pas perdre de vue que le compilateur de cette plateforme ne supporte pas forcément la dernière version de la norme C.

Le flag -std permet de forcer le compilateur à respecter une norme en particulier et s'assurer que le code sera compilable avec le compilateur de la plateforme.

Une bibliothèque tierce n'est pas forcément compatible avec toutes les normes. Si la norme utilisée pour compiler n'est pas compatible, il faut arrêter la compilation.

  • -std=c89 & -std=c90 définissent uniquement la macro __STDC__.
  • -std=c99 définit la macro __STD_VERSION__ avec la valeur 199901L
  • -std=c11 définit la macro __STD_VERSION__ avec la valeur 201112L
  • -std=c17 définit la macro __STD_VERSION__ avec la valeur 201710L

En utilisant des directives de précompilation, il est donc possible de vérifier la version de la norme utilisée pour compiler un code.

Voici un premier exemple qui nécessite d'utiliser l'option -std=c99 :

#include <stdio.h>

#if (__STDC_VERSION__ != 199901L)
#error "Ce code est compatible uniquement avec C99 !!!"
#endif

int main()
{
        printf("%ld\n",__STDC_VERSION__);
        return 0;
}

 

$ gcc std.c
error: "Ce code est compatible uniquement avec C99 !!!"
#error "Ce code est compatible uniquement avec C99 !!!"
 ^
1 error generated.
$
$ gcc std.c -std=c99
$ ./a.out 
199901
$

Dans ce second exemple, le standard C doit être au minimum c11 :

#include <stdio.h>

#if (__STDC_VERSION__ < 201112L)
#error "Ce code nécessite C11 !!!"
#endif

int main()
{
  printf("%ld\n",__STDC_VERSION__);
  return 0;
}

 

$ gcc std.c -std=c99
error: "Ce code nécessite C11 !!!"
#error "Ce code nécessite C11 !!!"
 ^
1 error generated.
$
$ gcc std.c
$ ./a.out 
201112
$
$ gcc std.c -std=c11
$ ./a.out 
201112
$
$ gcc std.c -std=c17
$ ./a.out 
201710
$

Ici, on peut constater que sans option, c'est la norme c11 qui est appliquée.

Compilation C++

Pour compiler du code C++, on utilise g++ au lieu de gcc et le tour est joué.

Les options de compilations vues ci-dessus sont les mêmes, mises à part les valeurs de l'option -std. La macro __cplusplus contient la valeur du standard.

  • c++98 : pour l'ISO C++ 1998, __cplusplus est définit à 199711L.
  • c++11 : pour l'ISO C++ 2011, __cplusplus est définit à 201103L.
  • c++14 : pour l'ISO C++ 2014, __cplusplus est définit à 201402L.
  • C++17 : pour l'ISO C++ 2017, __cplusplus est définit à 201703L.

Il existe également les extensions gnu : gnu++98, gnu++11, gnu++14 et gnu++17.

#include <iostream>

int main()
{
        std::cout << __cplusplus << std::endl;
}

 

$ g++ std.cpp ; ./a.out 
199711
$ g++ std.cpp -std=c++11 ; ./a.out 
201103
$ g++ std.cpp -std=c++14 ; ./a.out 
201402
$ g++ std.cpp -std=c++17 ; ./a.out 
201703
$

Il n'y a pas de return 0 à la fin de mon fichier .cpp. Ceci n'est pas une faute, la norme c++ définit un return 0 implicite à la fin de la fonction main.

Compilation de plusieurs fichiers

Pour compiler une application constituée de plusieurs fichiers c, il suffit d'indiquer la liste des fichiers à compiler : gcc -g -o myApp main.c mesfonctions.c -lm -I$HOME/MyInclude.

Cette méthode a plusieurs inconvénients :

  • Tous les fichiers sont recompilés à chaque fois. Pour un projet plus conséquent de 10, 20, 30 ou 100 fichiers, la compilation peut perdre du temps.
  • La ligne de commande devient abominable.
  • On modifie rarement la totalité des fichiers, il est donc inutile de les compiler tous à chaque fois.

L'idée est de compiler chaque fichier .c en .o avec l'option -c de gcc : gcc -c mesfonctions.c.

Lorsque tous les fichiers .c sont compilés, il reste qu'à les linker : gcc -g -o myApp main.o mesfonctions.o -lm -I$HOME/MyInclude.

Il reste un inconvénient, si l'on modifie un .c dont dépendent d'autres fichiers, il faut s'en souvenir est les recompiler également. En résumé, il faut gérer les dépendances entre fichiers.

Il existe des outils permettant de gérer presque automatiquement ces dépendances. Un des plus connus est make.


Série build : Compilation avec GCC - Écrire un Makefile