La compilation

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.

Principales suites de compilateurs

Les suites de compilateurs les plus connues pour les langages C/C++/Fortran sont :

GCC : gcc/g++/gfortran

  • suite libre
  • le plus répandu dans la communauté du logiciel libre
  • utilisé pour le noyau linux

LLVM : clang/clang++/(flang)

  • suite libre
  • écrit essentiellement en C++
  • réputé plus rapide et moins gourmand que GCC
  • préféré pour le multi-langage/multi-plateforme
  • interaction plus facile et complète avec l'AST (cf. cette section)
  • certaines incompatibilités (4% des paquets Debian ne compilent pas avec clang)

Intel : icc/icpc/ifort

  • suite propriétaire
  • réputé plus performant que GCC
  • contient des outils de performance et debugging

PGI : pgcc/pgc++/pgfortran

  • suite propriétaire mise à disposition gratuitement
  • supporte OpenACC qui est un langage haut-niveau pour la programmation des GPU
  • contient des outils de performance et debugging

Ce que fait vraiment un compilateur

Soit un fichier minimal helloworld.cpp contenant :

In [1]:
pygmentize -g helloworld.cpp
#include <iostream>

using namespace std;

int main() {
  // affiche Hello World !
  cout << "Hello World !" << endl;
  return 0;
}

Une compilation basique se fait en une ligne de commande pour produire l'exécutable a.out

In [2]:
g++ helloworld.cpp
./a.out
Hello World !

En réalité, cet appel au compilateur cache plusieurs étapes :

  1. Le preprocessing
  2. La compilation à proprement parler qui comprend :

    • l'analyse lexicale et sémantique => produit l'arbre de la syntaxe abstraite (AST pour abstract syntax tree)
    • la génération de code machine et son optimisation
    • l'assemblage du code machine en code objet
  1. L'édition de lien
In [3]:
source .notebooksrc

Le préprocesseur

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 :

In [4]:
g++ -E helloworld.cpp > helloworld.pp

Editer le fichier helloworld.pp pour constater la quantité de code additionnel introduite par le préprocesseur.

In [5]:
head helloworld.pp
printf ".\n.\n.\n"
tail helloworld.pp | pygmentize -g
# 1 "helloworld.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "helloworld.cpp"
# 1 "/usr/include/c++/9/iostream" 1 3
# 36 "/usr/include/c++/9/iostream" 3
       
# 37 "/usr/include/c++/9/iostream" 3
.
.
.
# 3 "helloworld.cpp"
using namespace std;

int main() {

  cout << "Hello World !" << endl;
  return 0;
}

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 :

In [6]:
echo $(wc -l helloworld.pp | awk '{print $1}') lignes !
28632 lignes !

Rappel : en C++, le préprocesseur est utilisé en particulier pour éviter l'inclusion multiple de fichiers d'en-tête.

In [7]:
pygmentize -g addition/addition.hpp
#ifndef ADDITION_HPP
#define ADDITION_HPP

int addition(int n, int m);

#endif
In [8]:
pygmentize -g addition/main.cpp
#include "addition.hpp"

int main(){
  return addition(1, 2);
}

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.

L'analyse lexicale

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.

L'analyse syntaxique

À 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 :

  • les nœuds internes représentent des opérateurs
  • les feuilles (ou nœuds externes) représentent les opérandes de ces opérateurs (variables ou constantes).

Exemple avec le programme minimal add.cpp :

In [9]:
pygmentize -g add.cpp
int addition(int m, int n);
int addition(int m, int n){
  return m + n;
}
int main(){
  return addition(1, 2);
}

On affiche l'AST correspondant en utilisant l'option -ast-dump du compilateur clang :

In [10]:
clang -Xclang -ast-dump -fsyntax-only add.cpp
TranslationUnitDecl 0x1380fa8 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0x1381880 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x1381540 '__int128'
|-TypedefDecl 0x13818f0 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x1381560 'unsigned __int128'
|-TypedefDecl 0x1381c68 <<invalid sloc>> <invalid sloc> implicit __NSConstantString '__NSConstantString_tag'
| `-RecordType 0x13819e0 '__NSConstantString_tag'
|   `-CXXRecord 0x1381948 '__NSConstantString_tag'
|-TypedefDecl 0x1381d00 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x1381cc0 'char *'
|   `-BuiltinType 0x1381040 'char'
|-TypedefDecl 0x13bed58 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list '__va_list_tag [1]'
| `-ConstantArrayType 0x13bed00 '__va_list_tag [1]' 1 
|   `-RecordType 0x1381df0 '__va_list_tag'
|     `-CXXRecord 0x1381d58 '__va_list_tag'
|-FunctionDecl 0x13bef20 <add.cpp:1:1, col:26> col:5 used addition 'int (int, int)'
| |-ParmVarDecl 0x13bedc8 <col:14, col:18> col:18 m 'int'
| `-ParmVarDecl 0x13bee48 <col:21, col:25> col:25 n 'int'
|-FunctionDecl 0x13bf150 prev 0x13bef20 <line:2:1, line:4:1> line:2:5 used addition 'int (int, int)'
| |-ParmVarDecl 0x13bf030 <col:14, col:18> col:18 used m 'int'
| |-ParmVarDecl 0x13bf0b0 <col:21, col:25> col:25 used n 'int'
| `-CompoundStmt 0x13bf2a0 <col:27, line:4:1>
|   `-ReturnStmt 0x13bf290 <line:3:3, col:14>
|     `-BinaryOperator 0x13bf270 <col:10, col:14> 'int' '+'
|       |-ImplicitCastExpr 0x13bf240 <col:10> 'int' <LValueToRValue>
|       | `-DeclRefExpr 0x13bf200 <col:10> 'int' lvalue ParmVar 0x13bf030 'm' 'int'
|       `-ImplicitCastExpr 0x13bf258 <col:14> 'int' <LValueToRValue>
|         `-DeclRefExpr 0x13bf220 <col:14> 'int' lvalue ParmVar 0x13bf0b0 'n' 'int'
`-FunctionDecl 0x13bf310 <line:5:1, line:7:1> line:5:5 main 'int ()'
  `-CompoundStmt 0x13bf500 <col:11, line:7:1>
    `-ReturnStmt 0x13bf4f0 <line:6:3, col:23>
      `-CallExpr 0x13bf4c0 <col:10, col:23> 'int'
        |-ImplicitCastExpr 0x13bf4a8 <col:10> 'int (*)(int, int)' <FunctionToPointerDecay>
        | `-DeclRefExpr 0x13bf460 <col:10> 'int (int, int)' lvalue Function 0x13bf150 'addition' 'int (int, int)'
        |-IntegerLiteral 0x13bf420 <col:19> 'int' 1
        `-IntegerLiteral 0x13bf440 <col:22> 'int' 2

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.

La génération de code et l'optimisation

À partir de l'arbre syntaxique, le compilateur crée du code intermédiaire qui va être :

  • linéarisé (passage d'une structure d'arbre à une séquence linéaires d'instructions)
  • optimisé pour s'exécuter plus rapidement

Un exemple inspiré de An Introduction to GCC (Brian Gough) pour illustrer l'effet du niveau d'optimisation :

In [11]:
pygmentize -g sum_power.cpp
#include <iostream>

double powern(double d, unsigned n) {
  double x = 1.0;
  unsigned j;
  for (j = 1; j <= n; j++)
    x *= d;
  return x;
}

int main() {
  double sum = 0.0;
  unsigned i;
  for (i = 1; i <= 200000000; i++) {
      sum += powern (i, i % 5);
    }
  std::cout<<"sum = "<<sum<<std::endl;
  return 0;
}

On teste les différents niveaux d'optimisation : O0, O1, O2, O3

In [12]:
for level in O0 O1 O2 O3
do
  echo "level:" $level
  g++ -$level sum_power.cpp
  time ./a.out
  echo 
done
level: O0
sum = 1.28e+40
1.72

level: O1
sum = 1.28e+40
0.87

level: O2
sum = 1.28e+40
0.38

level: O3
sum = 1.28e+40
0.40

On constate un facteur 5 sur le temps d'exécution entre O0 et O2 (ou O3).

L'assemblage

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 :

In [13]:
g++ -S add.cpp  # crée le fichier assembleur add.s
pygmentize -g add.s
	.file	"add.cpp"
	.text
	.globl	_Z8additionii
	.type	_Z8additionii, @function
_Z8additionii:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -4(%rbp)
	movl	%esi, -8(%rbp)
	movl	-4(%rbp), %edx
	movl	-8(%rbp), %eax
	addl	%edx, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	_Z8additionii, .-_Z8additionii
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$2, %esi
	movl	$1, %edi
	call	_Z8additionii
	nop
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8
	.long	 1f - 0f
	.long	 4f - 1f
	.long	 5
0:
	.string	 "GNU"
1:
	.align 8
	.long	 0xc0000002
	.long	 3f - 2f
2:
	.long	 0x3
3:
	.align 8
4:

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é :

In [14]:
g++ add.cpp
nm a.out
0000000000004010 B __bss_start
0000000000004010 b completed.8060
                 w __cxa_finalize@@GLIBC_2.2.5
0000000000004000 D __data_start
0000000000004000 W data_start
0000000000001070 t deregister_tm_clones
00000000000010e0 t __do_global_dtors_aux
0000000000003df8 d __do_global_dtors_aux_fini_array_entry
0000000000004008 D __dso_handle
0000000000003e00 d _DYNAMIC
0000000000004010 D _edata
0000000000004018 B _end
00000000000011d8 T _fini
0000000000001120 t frame_dummy
0000000000003df0 d __frame_dummy_init_array_entry
0000000000002154 r __FRAME_END__
0000000000003fc0 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
0000000000002004 r __GNU_EH_FRAME_HDR
0000000000001000 t _init
0000000000003df8 d __init_array_end
0000000000003df0 d __init_array_start
0000000000002000 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
00000000000011d0 T __libc_csu_fini
0000000000001160 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
0000000000001141 T main
00000000000010a0 t register_tm_clones
0000000000001040 T _start
0000000000004010 D __TMC_END__
0000000000001129 T _Z8additionii

Bien que le langage assembleur ne fasse pas l'objet de ce cours, on s'intéresse à un exemple de fichier assembleur (plus simple !) :

In [15]:
pygmentize -g hello.s
        .data
hello:  .asciiz "hello\n"   # hello pointe vers "hello\n\0"
        .text
        .globl __start
__start:
        li   $v0, 4         # la primitive print_string
        la   $a0, hello     # a0  l'adresse de hello
        syscall

Ce fichier peut s'exécuter directement avec spim :

In [16]:
spim -notrap -file hello.s
SPIM Version 8.0 of January 8, 2010
Copyright 1990-2010, James R. Larus.
All Rights Reserved.
See the file README for a full copyright notice.
hello
Attempt to execute non-instruction at 0x0040000c
  • L'assembleur est un programme (as sous linux) qui traduit le langage d’assembleur en langage machine.
  • Le résultat est un fichier objet (.o par convention).
  • En plus du code, ce fichier contient des informations qui permettent de lier (linker) le code de plusieurs programmes.

L'édition de liens

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 :

  • mets les fichiers objets les uns derrière les autres
  • résout les références symboliques entre ces fichiers et avec les bibliothèques externes

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.

Lien statique/lien dynamique

Lors de l'édition de lien, on distingue :

  • le lien avec les fichiers objets et les bibliothèques statiques qui sont directement inclus dans l'exécutable
  • le lien avec les bibliothèques dynamiques qui ne sont pas incluses dans l'exécutable. Le chemin de ces bibliothèques est fourni par le système d'exploitation qui utilise un mécanisme de recherche.

On peut lister les bibliothèques dynamiques avec la commande ldd (otool -L sous MacOS) :

In [17]:
g++ helloworld.cpp
ldd ./a.out
	linux-vdso.so.1 (0x00007ffcf01db000)
	libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f5a0f56f000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5a0f37d000)
	libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f5a0f22e000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f5a0f75d000)
	libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f5a0f213000)

Le processus de compilation en résumé

Extrait de C. Bastoul, (Ré)introduction à la compilation.

Peut-on détailler les étapes du compilateur 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 :

In [18]:
g++ -v helloworld.cpp
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:hsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 9.3.0-17ubuntu1~20.04' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-9-HskZEa/gcc-9-9.3.0/debian/tmp-nvptx/usr,hsa --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04) 
COLLECT_GCC_OPTIONS='-v' '-shared-libgcc' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/9/cc1plus -quiet -v -imultiarch x86_64-linux-gnu -D_GNU_SOURCE helloworld.cpp -quiet -dumpbase helloworld.cpp -mtune=generic -march=x86-64 -auxbase helloworld -version -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o /tmp/ccDjKHWg.s
GNU C++14 (Ubuntu 9.3.0-17ubuntu1~20.04) version 9.3.0 (x86_64-linux-gnu)
	compiled by GNU C version 9.3.0, GMP version 6.2.0, MPFR version 4.0.2, MPC version 1.1.0, isl version isl-0.22.1-GMP

GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring duplicate directory "/usr/include/x86_64-linux-gnu/c++/9"
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/9/include-fixed"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/9/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/include/c++/9
 /usr/include/x86_64-linux-gnu/c++/9
 /usr/include/c++/9/backward
 /usr/lib/gcc/x86_64-linux-gnu/9/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
GNU C++14 (Ubuntu 9.3.0-17ubuntu1~20.04) version 9.3.0 (x86_64-linux-gnu)
	compiled by GNU C version 9.3.0, GMP version 6.2.0, MPFR version 4.0.2, MPC version 1.1.0, isl version isl-0.22.1-GMP

GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 466f818abe2f30ba03783f22bd12d815
COLLECT_GCC_OPTIONS='-v' '-shared-libgcc' '-mtune=generic' '-march=x86-64'
 as -v --64 -o /tmp/ccScZ4Cf.o /tmp/ccDjKHWg.s
GNU assembler version 2.34 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.34
COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-shared-libgcc' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/9/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/9/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper -plugin-opt=-fresolution=/tmp/ccN5YZqi.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/9/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/9 -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/9/../../.. /tmp/ccScZ4Cf.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/9/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crtn.o
COLLECT_GCC_OPTIONS='-v' '-shared-libgcc' '-mtune=generic' '-march=x86-64'

On se rend compte que g++ est en réalité un pilote de compilateur qui déclenche 3 appels sucessifs :

  1. cc1plus : le compilateur C++ à proprement parler
  2. as : l'assembleur
  3. ld : le lieur (linker)

On décompose maintenant les 3 étapes en appelant les commandes correspondantes.

Etape 1 : conversion en assembleur

Le code source helloworld.cpp est converti par cc1plus en code assembleur helloworld.s :

In [19]:
# On extraie la ligne appelant 'cc1plus'
g++ -v helloworld.cpp 2>&1 >/dev/null | grep cc1plus
 /usr/lib/gcc/x86_64-linux-gnu/9/cc1plus -quiet -v -imultiarch x86_64-linux-gnu -D_GNU_SOURCE helloworld.cpp -quiet -dumpbase helloworld.cpp -mtune=generic -march=x86-64 -auxbase helloworld -version -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o /tmp/ccn18pBF.s

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.

In [20]:
# 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
bash: /usr/local/Cellar/gcc/8.3.0/libexec/gcc/x86_64-apple-darwin17.7.0/8.3.0/cc1plus: No such file or directory
helloworld.s: cannot open `helloworld.s' (No such file or directory)

À noter que cc1plus n'a pas vocation a être appelé directement : son répertoire ne figure pas dans la variable d'environnement PATH du shell.

Etape 2 : conversion en code objet

Le code assembleur helloworld.s est converti par as en code objet helloworld.o :

In [21]:
# On extraie la ligne appelant 'as'
g++ -v helloworld.cpp 2>&1 >/dev/null | grep "^ as"
 as -v --64 -o /tmp/ccdwc5me.o /tmp/cc5jzNei.s

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.

In [22]:
# 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
Assembler messages:
Fatal error: invalid listing option `r'
helloworld.o: cannot open `helloworld.o' (No such file or directory)

Etape 3 : édition de liens

Le code objet helloworld.o est lié par ld en un fichier exécutable a.out :

In [23]:
# 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.

In [24]:
# 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
/usr/bin/ld: unrecognised emulation mode: acosx_version_min
Supported emulations: elf_x86_64 elf32_x86_64 elf_i386 elf_iamcu elf_l1om elf_k1om i386pep i386pe
a.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=574e3e5ca8dbb250f1e7dfc79d472bb14a01c37e, for GNU/Linux 3.2.0, not stripped
Hello World !

Les options de compilation

Le compilateur c++ possède un très grand nombre d'options. Pour clang, l'option --help permet de toutes les lister :

In [25]:
echo 'clang++ propose' $(clang++ --help |wc -l) 'options !'
clang++ propose 872 options !

Les passer en revue ne présente pas d'intérêt. Les principales catégories sont :

  • l'ajustement en fonction l'architecture
  • les options d'alertes (warnings)
  • les options d'optimisation
  • les options de debuggage

Ajustement en fonction de l'architecture

En général, l'option -march=native fait ce travail d'ajustement pour vous :

In [26]:
g++ -c -Q -march=native --help=target
The following options are target specific:
  -m128bit-long-double        		[enabled]
  -m16                        		[disabled]
  -m32                        		[disabled]
  -m3dnow                     		[disabled]
  -m3dnowa                    		[disabled]
  -m64                        		[enabled]
  -m80387                     		[enabled]
  -m8bit-idiv                 		[disabled]
  -m96bit-long-double         		[disabled]
  -mabi=                      		sysv
  -mabm                       		[enabled]
  -maccumulate-outgoing-args  		[disabled]
  -maddress-mode=             		long
  -madx                       		[disabled]
  -maes                       		[enabled]
  -malign-data=               		compat
  -malign-double              		[disabled]
  -malign-functions=          		0
  -malign-jumps=              		0
  -malign-loops=              		0
  -malign-stringops           		[enabled]
  -mandroid                   		[disabled]
  -march=                     		bdver2
  -masm=                      		att
  -mavx                       		[enabled]
  -mavx2                      		[disabled]
  -mavx256-split-unaligned-load 	[disabled]
  -mavx256-split-unaligned-store 	[enabled]
  -mavx5124fmaps              		[disabled]
  -mavx5124vnniw              		[disabled]
  -mavx512bitalg              		[disabled]
  -mavx512bw                  		[disabled]
  -mavx512cd                  		[disabled]
  -mavx512dq                  		[disabled]
  -mavx512er                  		[disabled]
  -mavx512f                   		[disabled]
  -mavx512ifma                		[disabled]
  -mavx512pf                  		[disabled]
  -mavx512vbmi                		[disabled]
  -mavx512vbmi2               		[disabled]
  -mavx512vl                  		[disabled]
  -mavx512vnni                		[disabled]
  -mavx512vpopcntdq           		[disabled]
  -mbionic                    		[disabled]
  -mbmi                       		[enabled]
  -mbmi2                      		[disabled]
  -mbranch-cost=<0,5>         		2
  -mcall-ms2sysv-xlogues      		[disabled]
  -mcet-switch                		[disabled]
  -mcld                       		[disabled]
  -mcldemote                  		[disabled]
  -mclflushopt                		[disabled]
  -mclwb                      		[disabled]
  -mclzero                    		[disabled]
  -mcmodel=                   		[default]
  -mcpu=                      		
  -mcrc32                     		[disabled]
  -mcx16                      		[enabled]
  -mdispatch-scheduler        		[disabled]
  -mdump-tune-features        		[disabled]
  -mf16c                      		[enabled]
  -mfancy-math-387            		[enabled]
  -mfentry                    		[disabled]
  -mfentry-name=              		
  -mfentry-section=           		
  -mfma                       		[enabled]
  -mfma4                      		[disabled]
  -mforce-drap                		[disabled]
  -mforce-indirect-call       		[disabled]
  -mfp-ret-in-387             		[enabled]
  -mfpmath=                   		sse
  -mfsgsbase                  		[enabled]
  -mfunction-return=          		keep
  -mfused-madd                		
  -mfxsr                      		[enabled]
  -mgeneral-regs-only         		[disabled]
  -mgfni                      		[disabled]
  -mglibc                     		[enabled]
  -mhard-float                		[enabled]
  -mhle                       		[disabled]
  -miamcu                     		[disabled]
  -mieee-fp                   		[enabled]
  -mincoming-stack-boundary=  		0
  -mindirect-branch-register  		[disabled]
  -mindirect-branch=          		keep
  -minline-all-stringops      		[disabled]
  -minline-stringops-dynamically 	[disabled]
  -minstrument-return=        		none
  -mintel-syntax              		
  -mlarge-data-threshold=<number> 	65536
  -mlong-double-128           		[disabled]
  -mlong-double-64            		[disabled]
  -mlong-double-80            		[enabled]
  -mlwp                       		[disabled]
  -mlzcnt                     		[enabled]
  -mmanual-endbr              		[disabled]
  -mmemcpy-strategy=          		
  -mmemset-strategy=          		
  -mmitigate-rop              		[disabled]
  -mmmx                       		[enabled]
  -mmovbe                     		[enabled]
  -mmovdir64b                 		[disabled]
  -mmovdiri                   		[disabled]
  -mmpx                       		[disabled]
  -mms-bitfields              		[disabled]
  -mmusl                      		[disabled]
  -mmwaitx                    		[disabled]
  -mno-align-stringops        		[disabled]
  -mno-default                		[disabled]
  -mno-fancy-math-387         		[disabled]
  -mno-push-args              		[disabled]
  -mno-red-zone               		[disabled]
  -mno-sse4                   		[disabled]
  -mnop-mcount                		[disabled]
  -momit-leaf-frame-pointer   		[disabled]
  -mpc32                      		[disabled]
  -mpc64                      		[disabled]
  -mpc80                      		[disabled]
  -mpclmul                    		[enabled]
  -mpcommit                   		[disabled]
  -mpconfig                   		[disabled]
  -mpku                       		[disabled]
  -mpopcnt                    		[enabled]
  -mprefer-avx128             		
  -mprefer-vector-width=      		128
  -mpreferred-stack-boundary= 		0
  -mprefetchwt1               		[disabled]
  -mprfchw                    		[enabled]
  -mptwrite                   		[disabled]
  -mpush-args                 		[enabled]
  -mrdpid                     		[disabled]
  -mrdrnd                     		[enabled]
  -mrdseed                    		[disabled]
  -mrecip                     		[disabled]
  -mrecip=                    		
  -mrecord-mcount             		[disabled]
  -mrecord-return             		[disabled]
  -mred-zone                  		[enabled]
  -mregparm=                  		6
  -mrtd                       		[disabled]
  -mrtm                       		[disabled]
  -msahf                      		[enabled]
  -msgx                       		[disabled]
  -msha                       		[disabled]
  -mshstk                     		[disabled]
  -mskip-rax-setup            		[disabled]
  -msoft-float                		[disabled]
  -msse                       		[enabled]
  -msse2                      		[enabled]
  -msse2avx                   		[disabled]
  -msse3                      		[enabled]
  -msse4                      		[enabled]
  -msse4.1                    		[enabled]
  -msse4.2                    		[enabled]
  -msse4a                     		[enabled]
  -msse5                      		
  -msseregparm                		[disabled]
  -mssse3                     		[enabled]
  -mstack-arg-probe           		[disabled]
  -mstack-protector-guard-offset= 	
  -mstack-protector-guard-reg= 		
  -mstack-protector-guard-symbol= 	
  -mstack-protector-guard=    		tls
  -mstackrealign              		[disabled]
  -mstringop-strategy=        		[default]
  -mstv                       		[enabled]
  -mtbm                       		[disabled]
  -mtls-dialect=              		gnu
  -mtls-direct-seg-refs       		[enabled]
  -mtune-ctrl=                		
  -mtune=                     		bdver2
  -muclibc                    		[disabled]
  -mvaes                      		[disabled]
  -mveclibabi=                		[default]
  -mvect8-ret-in-mem          		[disabled]
  -mvpclmulqdq                		[disabled]
  -mvzeroupper                		[enabled]
  -mwaitpkg                   		[disabled]
  -mwbnoinvd                  		[disabled]
  -mx32                       		[disabled]
  -mxop                       		[disabled]
  -mxsave                     		[enabled]
  -mxsavec                    		[disabled]
  -mxsaveopt                  		[disabled]
  -mxsaves                    		[disabled]

  Known assembler dialects (for use with the -masm= option):
    att intel

  Known ABIs (for use with the -mabi= option):
    ms sysv

  Known code models (for use with the -mcmodel= option):
    32 kernel large medium small

  Valid arguments to -mfpmath=:
    387 387+sse 387,sse both sse sse+387 sse,387

  Known indirect branch choices (for use with the -mindirect-branch=/-mfunction-return= options):
    keep thunk thunk-extern thunk-inline

  Known choices for return instrumentation with -minstrument-return=:
    call none nop5

  Known data alignment choices (for use with the -malign-data= option):
    abi cacheline compat

  Known vectorization library ABIs (for use with the -mveclibabi= option):
    acml svml

  Known address mode (for use with the -maddress-mode= option):
    long short

  Known preferred register vector length (to use with the -mprefer-vector-width= option):
    128 256 512 none

  Known stack protector guard (for use with the -mstack-protector-guard= option):
    global tls

  Valid arguments to -mstringop-strategy=:
    byte_loop libcall loop rep_4byte rep_8byte rep_byte unrolled_loop
    vector_loop

  Known TLS dialects (for use with the -mtls-dialect= option):
    gnu gnu2

  Known valid arguments for -march= option:
    i386 i486 i586 pentium lakemont pentium-mmx winchip-c6 winchip2 c3 samuel-2 c3-2 nehemiah c7 esther i686 pentiumpro pentium2 pentium3 pentium3m pentium-m pentium4 pentium4m prescott nocona core2 nehalem corei7 westmere sandybridge corei7-avx ivybridge core-avx-i haswell core-avx2 broadwell skylake skylake-avx512 cannonlake icelake-client icelake-server cascadelake bonnell atom silvermont slm goldmont goldmont-plus tremont knl knm intel geode k6 k6-2 k6-3 athlon athlon-tbird athlon-4 athlon-xp athlon-mp x86-64 eden-x2 nano nano-1000 nano-2000 nano-3000 nano-x2 eden-x4 nano-x4 k8 k8-sse3 opteron opteron-sse3 athlon64 athlon64-sse3 athlon-fx amdfam10 barcelona bdver1 bdver2 bdver3 bdver4 znver1 znver2 btver1 btver2 generic native

  Known valid arguments for -mtune= option:
    generic i386 i486 pentium lakemont pentiumpro pentium4 nocona core2 nehalem sandybridge haswell bonnell silvermont goldmont goldmont-plus tremont knl knm skylake skylake-avx512 cannonlake icelake-client icelake-server cascadelake intel geode k6 athlon k8 amdfam10 bdver1 bdver2 bdver3 bdver4 btver1 btver2 znver1 znver2

Niveaux d'alerte

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 :

In [27]:
pygmentize -g fonc.cpp
int f(int a, int b) {
  int c;
  if(c > a) return 1;
  else return 0;
}
In [28]:
g++ -c fonc.cpp

g++ compile ce fichier silencieusement alors qu'il contient du mauvais code :

In [29]:
g++ -Wall -c fonc.cpp
fonc.cpp: In function ‘int f(int, int)’:
fonc.cpp:3:3: warning: ‘c’ is used uninitialized in this function [-Wuninitialized]
    3 |   if(c > a) return 1;
      |   ^~

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.

In [30]:
g++ -Wall -Wextra -c fonc.cpp
fonc.cpp: In function ‘int f(int, int)’:
fonc.cpp:1:18: warning: unused parameter ‘b’ [-Wunused-parameter]
    1 | int f(int a, int b) {
      |              ~~~~^
fonc.cpp:3:3: warning: ‘c’ is used uninitialized in this function [-Wuninitialized]
    3 |   if(c > a) return 1;
      |   ^~

Options d'optimisation et de débuggage

Optimisation

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.

Débuggage

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.

La création et utilisation de bibliothèques

  • Il est courant de regrouper les fichiers objets au sein d'un fichier unique appelé bibliothèque.
  • Cette bibliothèque est en réalité une archive construite avec la commande ar
  • le contenu des fichiers est mis l'un à la suite de l'autre dans un fichier libmoncontenu.a

Prenons pour exemple l'ensemble de fichiers sources contenus dans maillage/ :

In [31]:
tree maillage
maillage
├── maillage.hpp
├── main.cpp
├── Makefile
├── Makefile_1
├── Makefile_2
├── Makefile_3
├── Makefile_4
├── Makefile_a
├── Makefile_arch
├── point.cpp
├── point.hpp
├── polygone.cpp
├── polygone.hpp
├── quadrangle.cpp
├── quadrangle.hpp
├── segment.cpp
├── segment.hpp
├── triangle.cpp
└── triangle.hpp

0 directories, 19 files

Compilons d'abord en utilisant le Makefile du répertoire.

In [32]:
cd $ROOTDIR
cd maillage
make clean
make
rm -f main.e *.o *.a *.d
g++ -std=c++11 -Wall -o main.o -c main.cpp
g++ -std=c++11 -Wall -o point.o -c point.cpp
g++ -std=c++11 -Wall -o segment.o -c segment.cpp
g++ -std=c++11 -Wall -o triangle.o -c triangle.cpp
g++ -std=c++11 -Wall -o quadrangle.o -c quadrangle.cpp
g++ -std=c++11 -Wall -o polygone.o -c polygone.cpp
g++ -std=c++11 -Wall -o main.e main.o point.o segment.o triangle.o quadrangle.o polygone.o

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 :

In [33]:
ar r libmaillage.a point.o segment.o triangle.o quadrangle.o polygone.o
file libmaillage.a
ar: creating libmaillage.a
libmaillage.a: current ar archive

On fait ensuite l'édition de lien avec cette bibliothèque libmaillage.a :

In [34]:
c++ -std=c++11 -Wall -o main.e main.o -L. -lmaillage
./main.e
Test cout sur maillage :
Type de mailles : triangles
Nombre max de voisins : 3
Nombre de noeuds : 4
Nombre de mailles : 2

 Tests unitaires réussis, maillage : 4/4

Remarques :

  • Notez la syntaxe du nom de bibliothèque derrière l'option -l: maillage pour un fichier bibliothèque qui s'appelle libmaillage.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 :

In [35]:
ar t libmaillage.a
point.o
segment.o
triangle.o
quadrangle.o
polygone.o

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.

In [36]:
cd $ROOTDIR/test
pygmentize test_maillage.cpp
#include "quadrangle.hpp"
#include "maillage.hpp"

int main() {
    maillage<quadrangle, 4> m;
    m.all_testsu();
    return 0;
}

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

In [37]:
c++ -std=c++11 -Wall -I../maillage  -c test_maillage.cpp
file test_maillage.o
test_maillage.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

On compile maintenant en liant avec la bibliothèque libmaillage.a et on exécute le programme :

In [38]:
c++ -std=c++11 -Wall -I./maillage/ -o test_maillage.e test_maillage.o \
    -L../maillage -lmaillage 
./test_maillage.e
Test cout sur maillage :
Type de mailles : triangles
Nombre max de voisins : 3
Nombre de noeuds : 4
Nombre de mailles : 2

 Tests unitaires réussis, maillage : 4/4

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.

Conclusion

On a vu :

  • les étapes du processus de compilation
  • quelques options de compilation
  • la création et l'utilisation des bibliothèques

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.