Un compilateur est un programme qui traduit votre code source en un fichier contenant des instructions exécutables par le système d'exploitation de votre ordinateur. La compilation est un processus complexe. En sciences de l'informatique, elle constitue un domaine de recherche à part entière.
clang/clang++/(flang)
¶clang
)Soit un fichier minimal helloworld.cpp
contenant :
pygmentize -g helloworld.cpp
Une compilation basique se fait en une ligne de commande pour produire l'exécutable a.out
g++ helloworld.cpp
./a.out
En réalité, cet appel au compilateur cache plusieurs étapes :
La compilation à proprement parler qui comprend :
source .notebooksrc
Le préprocesseur parcourt l'ensemble du code source que vous avez écrit et interprète les directives de précompilation commençant par #
telle que :
# include <math.h>
On peut demander au compilateur d'afficher le code ainsi traité en utilisant l'option -E
:
g++ -E helloworld.cpp > helloworld.pp
Editer le fichier helloworld.pp pour constater la quantité de code additionnel introduite par le préprocesseur.
head helloworld.pp
printf ".\n.\n.\n"
tail helloworld.pp | pygmentize -g
On reconnaît notre code source à la fin (sans les commentaires), précédé par un très grand nombre de lignes.
En réalité, c'est la simple directive
#include <iostream>
qui provoque l'inclusion du fichier header iostream
qui lui-même inclut d'autres headers (ios
, streambuf
, etc.), eux-même incluant d'autres headers, etc. De sorte que le fichier final fait :
echo $(wc -l helloworld.pp | awk '{print $1}') lignes !
Rappel : en C++, le préprocesseur est utilisé en particulier pour éviter l'inclusion multiple de fichiers d'en-tête.
pygmentize -g addition/addition.hpp
pygmentize -g addition/main.cpp
En effet, sans la directive #ifndef
, le fichier addition.hpp
peut être inclus dans un autre fichier source, ce qui peut provoquer une erreur de compilation.
Elle partitionne le code issu du préprocesseur en jetons lexicaux. Peut souvent se formaliser par une grammaire d'expressions régulières à l'aide d'un automate fini.
À partir de la liste de jetons lexicaux, elle reconnait la structure syntaxique du code à partir de la description grammaticale du langage pour constuire l'arbre syntaxique (AST, pour abstract syntaxic tree) dont :
Exemple avec le programme minimal add.cpp
:
pygmentize -g add.cpp
On affiche l'AST correspondant en utilisant l'option -ast-dump
du compilateur clang
:
clang -Xclang -ast-dump -fsyntax-only add.cpp
On distingue les deux fonctions main
et addition
qui sont des noeuds de l'arbre.
Aux extrémités des branches correspondantes, les feuilles sont les variables m
et n
et les constantes 1
et 2
.
L'AST est la représentation intermédiaire interne du programme est utilisée lors de la phase d'optimisation et de génération de code assembleur.
À partir de l'arbre syntaxique, le compilateur crée du code intermédiaire qui va être :
Un exemple inspiré de An Introduction to GCC (Brian Gough) pour illustrer l'effet du niveau d'optimisation :
pygmentize -g sum_power.cpp
On teste les différents niveaux d'optimisation : O0, O1, O2, O3
for level in O0 O1 O2 O3
do
echo "level:" $level
g++ -$level sum_power.cpp
time ./a.out
echo
done
On constate un facteur 5 sur le temps d'exécution entre O0
et O2
(ou O3
).
En manipulant le code intermédiaire, l'étape de génération de code produit du code assembleur : un code texte humainement lisible mais difficile à interpréter (et à écrire directement !) car il est très proche du code machine. Il possède quasiment les mêmes instructions que le code machine qui lui est représenté par des mots (nombres binaires).
Pour exporter la traduction en assembleur d'un fichier source, on utilise l'option -S
:
g++ -S add.cpp # crée le fichier assembleur add.s
pygmentize -g add.s
On reconnaît en particulier 2 étiquettes _Z8additionii
et _main
qui représente nos 2 fonctions.
En utilisant la commande nm
, on retrouve ces noms de symboles dans l'exécutable généré :
g++ add.cpp
nm a.out
Bien que le langage assembleur ne fasse pas l'objet de ce cours, on s'intéresse à un exemple de fichier assembleur (plus simple !) :
pygmentize -g hello.s
Ce fichier peut s'exécuter directement avec spim
:
spim -notrap -file hello.s
as
sous linux) qui traduit le langage d’assembleur en langage machine..o
par convention).C'est une étape qui consiste à transformer un ensemble de fichiers objets en un exécutable.
L'éditeur de liens est un programme (ld
sous linux) qui utilise tous les fichiers objets pour fabriquer l’exécutable :
C’est enfin le système d’exploitation qui s'occupera (à la demande de l'utilisateur) de charger en mémoire et lancer le programme.
Lors de l'édition de lien, on distingue :
On peut lister les bibliothèques dynamiques avec la commande ldd
(otool -L
sous MacOS) :
g++ helloworld.cpp
ldd ./a.out
Extrait de C. Bastoul, (Ré)introduction à la compilation.
g++
?¶L'appel à la commande g++
semble faire toutes les étapes du processus complet de compilation.
Comment détailler ces étapes ?
On va d'abord appeller g++
en mode verbeux (option -v
) pour afficher les sous-étapes :
g++ -v helloworld.cpp
On se rend compte que g++
est en réalité un pilote de compilateur qui déclenche 3 appels sucessifs :
cc1plus
: le compilateur C++ à proprement parleras
: l'assembleurld
: le lieur (linker)On décompose maintenant les 3 étapes en appelant les commandes correspondantes.
Le code source helloworld.cpp
est converti par cc1plus
en code assembleur helloworld.s
:
# On extraie la ligne appelant 'cc1plus'
g++ -v helloworld.cpp 2>&1 >/dev/null | grep cc1plus
On constate que le compilateur crée un fichier assembleur .s
temporaire.
On remplace par helloworld.s
et on exécute la commande cc1plus
et ses arguments.
# commande produite par le compilateur g++-8 sur MacOS
/usr/local/Cellar/gcc/8.3.0/libexec/gcc/x86_64-apple-darwin17.7.0/8.3.0/cc1plus \
-quiet \
-D__DYNAMIC__ \
helloworld.cpp \
-fPIC \
-quiet \
-dumpbase helloworld.cpp \
-mmacosx-version-min=10.13.0 \
-mtune=core2 \
-auxbase helloworld \
-o helloworld.s
file helloworld.s
À noter que
cc1plus
n'a pas vocation a être appelé directement : son répertoire ne figure pas dans la variable d'environnementPATH
du shell.
Le code assembleur helloworld.s
est converti par as
en code objet helloworld.o
:
# On extraie la ligne appelant 'as'
g++ -v helloworld.cpp 2>&1 >/dev/null | grep "^ as"
On constate que le compilateur crée un fichier objet .o
temporaire à partir du .s
temporaire.
On remplace par helloworld.o
et helloworld.s
puis on exécute la commande as
et ses arguments.
# commande produite par le compilateur g++-8 sur MacOS
as -arch x86_64 \
-force_cpusubtype_ALL \
-mmacosx-version-min=10.13 \
-o helloworld.o \
helloworld.s
file helloworld.o
Le code objet helloworld.o
est lié par ld
en un fichier exécutable a.out
:
# On extraie la ligne appelant 'ld'
g++ -v helloworld.cpp 2>&1 >/dev/null | grep "/usr/bin/ld"
On constate que le compilateur a utilisé le fichier objet .o
temporaire pour créer l'exécutable a.out
.
On remplace par helloworld.o
puis on exécute la commande ld
et ses arguments.
# commande produite par le compilateur g++-8 sur MacOS
/usr/bin/ld -dynamic \
-arch x86_64 \
-macosx_version_min 10.13.0 \
-weak_reference_mismatches non-weak \
-o a.out \
-L/usr/local/lib \
-L. \
-L/usr/local/Cellar/gcc/8.3.0/lib/gcc/8/gcc/x86_64-apple-darwin17.7.0/8.3.0 \
-L/usr/local/Cellar/gcc/8.3.0/lib/gcc/8/gcc/x86_64-apple-darwin17.7.0/8.3.0/../../.. \
helloworld.o \
-lstdc++ \
-no_compact_unwind \
-lSystem \
-lgcc_ext.10.5 \
-lgcc \
-lSystem
file a.out
./a.out
Le compilateur c++
possède un très grand nombre d'options.
Pour clang
, l'option --help
permet de toutes les lister :
echo 'clang++ propose' $(clang++ --help |wc -l) 'options !'
Les passer en revue ne présente pas d'intérêt. Les principales catégories sont :
En général, l'option -march=native
fait ce travail d'ajustement pour vous :
g++ -c -Q -march=native --help=target
Connaître les options de base pour les warnings permet d'éviter des erreurs de programmation et de gagner beaucoup de temps. En général, le compilateur active par défaut un grand nombre de warnings mais c'est insuffisant dans certains cas.
Soit le fichier fonc.cpp
contenant :
pygmentize -g fonc.cpp
g++ -c fonc.cpp
g++
compile ce fichier silencieusement alors qu'il contient du mauvais code :
g++ -Wall -c fonc.cpp
Ici -Wall
permet de détecter le fait que c
est utilisé sans avoir été initialisé mais il faut ajouter Wextra
pour détecter le paramètre inutilisé dans la signature de la fonction.
g++ -Wall -Wextra -c fonc.cpp
Nous avons déjà eu un aperçu des options d'optimisation :
-On avec n=0,1,2,3,s,fast
Attention, en fonction du compilateur, un niveau élevé peut augmenter significativement le temps de compilation et altérer la correction du résultat ! $\rightarrow$ en phase de débuggage, il faut baisser le niveau d'optimisation.
Les options de debuggage vont ajouter des informations utiles pour les débuggueurs :
-gn avec n=0,1,2,3 (`2` par défaut)
On reviendra dessus dans la partie du cours consacrée au débuggage.
ar
libmoncontenu.a
Prenons pour exemple l'ensemble de fichiers sources contenus dans maillage/
:
tree maillage
Compilons d'abord en utilisant le Makefile
du répertoire.
cd $ROOTDIR
cd maillage
make clean
make
La compilation a créé un exécutable en liant les fichiers objets main.o point.o segment.o triangle.o quadrangle.o polygone.o
.
Une autre façon de faire consiste à regrouper d'abord les fichiers objets (excepté main.o
) dans une bibliothèque .a
:
ar r libmaillage.a point.o segment.o triangle.o quadrangle.o polygone.o
file libmaillage.a
On fait ensuite l'édition de lien avec cette bibliothèque libmaillage.a
:
c++ -std=c++11 -Wall -o main.e main.o -L. -lmaillage
./main.e
Remarques :
- Notez la syntaxe du nom de bibliothèque derrière l'option
-l
:maillage
pour un fichier bibliothèque qui s'appellelibmaillage.a
- le fichier
main.o
doit être placé avant l'invocation de la bibliothèque-lmaillage
On peut lister le contenu de cette bibliothèque statique :
ar t libmaillage.a
L'intérêt d'une bibliothèque est de pouvoir accéder à du contenu de code depuis une autre application.
Par exemple, on se place dans un répertoire extérieur au projet maillage
.
cd $ROOTDIR/test
pygmentize test_maillage.cpp
On compile d'abord le fichier test_maillage.cpp
en indiquant le répertoire où se trouvent les fichiers d'en-têtes inclus (ici quadrangle.hpp
et maillage.hpp
).
c++ -std=c++11 -Wall -I../maillage -c test_maillage.cpp
file test_maillage.o
On compile maintenant en liant avec la bibliothèque libmaillage.a
et on exécute le programme :
c++ -std=c++11 -Wall -I./maillage/ -o test_maillage.e test_maillage.o \
-L../maillage -lmaillage
./test_maillage.e
Notez l'argument -L../maillage
qui indique à l'éditeur de lien qu'il doit ajouter le répertoire ../maillage/
à la liste des répertoires où il va rechercher le fichier libmaillage.a
.
On a vu :
Au programme du prochain chapitre : les gestionnaires de projets, c'est-à-dire les outils qui vont appeler pour vous le compilateur pour produire efficacement un ou plusieurs exécutables à partir de votre code source.