Notes sur le processus de compilation avec GCC/Clang

Cet article de blog est surtout destiné à me servir de notes, pour expliquer le processus de compilation d’un programme dans mon cours C++. Il est basé sur la vidéo : « Hello, world? » — What really happens when you compile & run a simple program.

N’hésitez pas à partager d’autres sources ou informations sur le processus de compilation.

Pour réaliser ces tests, il faut créer un simple programme hello world :

#include <iostream>

int main() {
    std::cout << "Hello, world!" << std::endl;
    return 0;
}

Il est possible de réaliser ces tests dans coliru, ce qui est probablement plus simple pour que les débutants puissent faire les tests eux-mêmes. Pour que chacun puisse reproduire les tests, je donne la ligne de commande dans coliru, donc avec l’appel de clang++ avant (si on réalise ces tests sur un ordinateur, les fichiers ne sont pas supprimés entre chaque lancement de la ligne de commande, il n’est pas nécessaire de recompiler à chaque fois pour obtenir les fichiers intermédiaires).

Remarque générale

Il est généralement possible de connaitre la version d’une application en ligne de commande avec l’option « -v » ou « –version » et l’aide avec l’option « -h » ou « –help ».

clang++ -v
clang++ --help

affiche :

clang version 3.5.0 (tags/RELEASE_350/final 217394)
Target: x86_64-unknown-linux-gnu
Thread model: posix
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu...
Found candidate GCC installation: /usr/local/bin/../lib/gcc/x86...
Found candidate GCC installation: /usr/local/bin/../lib/gcc/x86...
Found candidate GCC installation: /usr/local/bin/../lib/gcc/x86...
Selected GCC installation: /usr/local/bin/../lib/gcc/x86_64-unk...
Candidate multilib: .;@m64
Selected multilib: .;@m64

et

OVERVIEW: clang LLVM compiler
USAGE: clang [options] <inputs>
OPTIONS:
 -### Print (but do not run) the commands to run for this...
 --analyze Run the static analyzer
 -arcmt-migrate-emit-errors
 Emit ARC errors even if the migrator can fix them
 -arcmt-migrate-report-output <value>
 Output path for the plist report
 -cxx-isystem <directory>
 Add directory to the C++ SYSTEM include search path
 -c Only run preprocess, compile, and assemble steps

... plus de 300 lignes

Les étapes de compilation

La compilation d’une application C++ est réalisé en plusieurs étapes. Ces différentes étapes sont lancées par des applications en ligne de commande. Lorsque l’on compile un programme (par exemple dans un IDE comme Qt Creator ou Visual C++, avec un système de compilation comme Cmake ou en ligne de commande en appelant directement Clang ou GCC), cela appelle en fait plusieurs programmes et génère plusieurs fichiers.

Je ne vais pas entrer dans le fonctionnement des outils de compilation « haut niveau » comme les IDE ou les systèmes de build, mais aborder directement ce qu’il se passe lorsque l’on appelle le compilateur. Cet article prend Clang comme exemple, mais le principe est le même avec GCC, Visual C++, Inter Compiler, etc. (mais bien sûr, les commandes appelées ne seront pas forcement identiques).

La compilation d’un fichier « main.cpp » avec Clang est relativement simple, il suffit d’appeler une seule ligne de commande (la seconde ligne permet de lancer l’application créée, qui s’appelle « a.out » par défaut) :

clang++ main.cpp   # compilation
a.out              # exécution

La compilation appelle en fait d’autres commandes. Pour avoir le détail des commandes appelées :

clang++ main.cpp -v

affiche :

clang version 3.5.0 (tags/RELEASE_350/final 217394)
Target: x86_64-unknown-linux-gnu
Thread model: posix
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-...
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-...
Found candidate GCC installation: /usr/local/bin/../lib/gcc/...
Found candidate GCC installation: /usr/local/bin/../lib/gcc/...
Found candidate GCC installation: /usr/local/bin/../lib/gcc/...
Selected GCC installation: /usr/local/bin/../lib/gcc/x86_64-...
Candidate multilib: .;@m64
Selected multilib: .;@m64
 "/usr/local/bin/clang" -cc1 -triple x86_64-unknown-linux-gnu...
clang -cc1 version 3.5.0 based upon LLVM 3.5.0 default target...
ignoring nonexistent directory "/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2/...
 /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2/...
 /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2/...
 /usr/local/include
 /usr/local/bin/../lib/clang/3.5.0/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
 "/usr/bin/ld" --eh-frame-hdr -m elf_x86_64 -dynamic-linker...

Compile, link and run...
Share!

Clang affiche beaucoup de messages d’information. Si vous utilisez un IDE, vous n’avez probablement pas l’habitude de voir ces messages, ils sont souvent cachés dans une fenêtre de sortie. Ils sont très utiles lorsque l’on a un problème de compilation, il ne faut pas hésiter à les consulter dès que l’on a un problème.

J’ai affiché en rouge deux lignes importantes. La première (« cc1 » pour « clang compiler ») permet de lancer la compilation du fichier « .cpp » en fichier binaire. La seconde (« ld » pour « linker ») permet de lier les fichiers binaires entre eux pour créer un programme.

L’option « -save-temps » permet d’enregistrer les étapes intermédiaires de compilation dans des fichiers :

clang++ main.cpp -save-temps && ls -l

affiche :

total 492
-rwxr-xr-x 1 2001 2000   8279 Mar 7 19:34 a.out
-rw-rw-rw- 1 2001 2000     97 Mar 7 19:34 main.cpp
-rw-r--r-- 1 2001 2000 472192 Mar 7 19:34 main.ii
-rw-r--r-- 1 2001 2000   2864 Mar 7 19:34 main.o
-rw-r--r-- 1 2001 2000   2235 Mar 7 19:34 main.s

Les fichiers sont générés dans l’ordre suivant :

  • main.ii (code source après pré-processeur) ;
  • main.s (code assembleur) ;
  • main.o (fichier objet) ;
  • a.out (fichier exécutable)

Pré-processeur

Le pré-processeur lit le fichier source « .cpp » (au format texte) et remplace chaque « #include » par le contenu du fichier correspondant. Le résultat est le fichier « .ii »  (au format texte).

Pour afficher les 1000 premières lignes de ce fichier :

clang++ main.cpp -save-temps && head -n 1000 main.ii

affiche :

# 1 "main.cpp"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 318 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.cpp" 2
# 1 "/usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2/...
# 37 "/usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2...

# 1 "/usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2/...
# 186 "/usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9...

namespace std
{
 typedef long unsigned int size_t;
 typedef long int ptrdiff_t;
}

... beaucoup de lignes

Pour connaitre le nombre de ligne de ce fichier :

clang++ main.cpp -save-temps && wc -l main.ii

affiche :

17604 main.ii

Le code source est donc passé de six lignes dans le « .cpp » à plus de 17 mille lignes dans le « .ii ».

Ce fichier est le résultat de l’inclusion de nombreux fichiers. Pour connaître la liste :

clang++ main.cpp -H

affiche :

. /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2/...
.. /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2/...
... /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2/...
.... /usr/include/features.h
..... /usr/include/x86_64-linux-gnu/bits/predefs.h
..... /usr/include/x86_64-linux-gnu/sys/cdefs.h
...... /usr/include/x86_64-linux-gnu/bits/wordsize.h
..... /usr/include/x86_64-linux-gnu/gnu/stubs.h
...... /usr/include/x86_64-linux-gnu/bits/wordsize.h
...... /usr/include/x86_64-linux-gnu/gnu/stubs-64.h
... /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2/...
.. /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2/...
... /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2/...
.... /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2/...
..... /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/4.9.2/...
...... /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/...
..... /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/...
...... /usr/local/bin/../lib/gcc/x86_64-unknown-linux-gnu/...
... 88 fichiers inclus

La compilation

L’étape suivante permet de prendre le code source (compréhensible par un humain) et de le convertir en code binaire (compréhensible par un ordinateur). Le code est dans un premier temps converti en langage bas niveau (compréhensible par un humain… mais beaucoup plus difficilement, c’est un langage plus proche du langage des ordinateurs).

Le code assembleur généré (« symbolic langage source code ») est enregistré dans le fichier « main.s » :

clang++ main.cpp -save-temps && wc -l main.s

affiche :

100 main.s

Le fichier contient 100 lignes. Pour voir le contenu :

clang++ main.cpp -save-temps && less main.s

affiche :

        .text
        .file "main.ii"
        .section .text.startup,"ax",@progbits
        .align 16, 0x90
        .type __cxx_global_var_init,@function
__cxx_global_var_init: # @__cxx_global_var_init
        .cfi_startproc
# BB#0:
        pushq %rbp
.Ltmp0:
        .cfi_def_cfa_offset 16
.Ltmp1:
        .cfi_offset %rbp, -16
        movq %rsp, %rbp
.Ltmp2:
        .cfi_def_cfa_register %rbp
        subq $16, %rsp
        movabsq $_ZStL8__ioinit, %rdi
        callq _ZNSt8ios_base4InitC1Ev
        movabsq $_ZNSt8ios_base4InitD1Ev, %rdi

... 100 lignes

La compréhension de l’assembleur n’est plus indispensable pour comprendre le C++ (en fonction de votre domaine d’application) et cela sort d’un cours de base, mais si vous voulez plus de détails, vous pouvez consulter la série d’articles Programme d’étude sur le C++ bas niveau.

Ce fichier de source assembleur (au format texte) est ensuite compilé dans le fichier objet « .o » (au format binaire). Ce fichier n’est pas destiné à être lu par des humains, il n’est pas donc pas possible de l’ouvrir directement en format texte (cela donne principalement une succession de caractères qui semblent aléatoire – excepté pour les chaînes de caractères qui se trouvaient dans le code C++ et que l’on peut retrouver dans le fichier objet).

La commande « file » permet d’avoir des informations sur ce fichier objet :

clang++ main.cpp -save-temps && file main.o

affiche :

main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (GNU/Linux),
not stripped

ELF (Executable and Linkable Format) est un format de fichier utilisé sur Linux, qui permet d’indiquer au système ce que contient ce fichier. On peut avoir plus de détails avec la commande « readelf » :

clang++ main.cpp -save-temps && readelf -h main.o

affiche :

ELF Header:
 Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 
 Class:                              ELF64
 Data:                               2's complement, little endian
 Version:                            1 (current)
 OS/ABI:                             UNIX - GNU
 ABI Version:                        0
 Type:                               REL (Relocatable file)
 Machine:                            Advanced Micro Devices X86-64
 Version:                            0x1
 Entry point address:                0x0
 Start of program headers:           0 (bytes into file)
 Start of section headers:           576 (bytes into file)
 Flags:                              0x0
 Size of this header:                64 (bytes)
 Size of program headers:            0 (bytes)
 Number of program headers:          0
 Size of section headers:            64 (bytes)
 Number of section headers:          17
 Section header string table index:  14

Pour afficher le contenu du fichier binaire :

clang++ main.cpp -save-temps && hexdump -C main.o

affiche :

00000000 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 |.ELF............|
00000010 01 00 3e 00 01 00 00 00 00 00 00 00 00 00 00 00 |..>.............|
00000020 00 00 00 00 00 00 00 00 40 02 00 00 00 00 00 00 |........@.......|
00000030 00 00 00 00 40 00 00 00 00 00 40 00 11 00 0e 00 |....@.....@.....|
00000040 55 48 89 e5 48 83 ec 10 48 bf 00 00 00 00 00 00 |UH..H...H.......|
00000050 00 00 48 be 00 00 00 00 00 00 00 00 c7 45 fc 00 |..H..........E..|
00000060 00 00 00 e8 00 00 00 00 48 be 00 00 00 00 00 00 |........H.......|
00000070 00 00 48 89 c7 e8 00 00 00 00 b9 00 00 00 00 48 |..H............H|
00000080 89 45 f0 89 c8 48 83 c4 10 5d c3 00 00 00 00 00 |.E...H...]......|
00000090 55 48 89 e5 48 83 ec 10 48 bf 00 00 00 00 00 00 |UH..H...H.......|
000000a0 00 00 e8 00 00 00 00 48 bf 00 00 00 00 00 00 00 |.......H........|
000000b0 00 48 be 00 00 00 00 00 00 00 00 48 ba 00 00 00 |.H.........H....|
000000c0 00 00 00 00 00 e8 00 00 00 00 89 45 fc 48 83 c4 |...........E.H..|
000000d0 10 5d c3 66 66 66 66 2e 0f 1f 84 00 00 00 00 00 |.].ffff.........|
000000e0 55 48 89 e5 e8 a7 ff ff ff 5d c3 48 65 6c 6c 6f |UH.......].Hello|
000000f0 2c 20 77 6f 72 6c 64 21 00 00 00 00 00 00 00 00 |, world!........|
00000100 00 00 00 00 00 00 00 00 00 63 6c 61 6e 67 20 76 |.........clang v|
00000110 65 72 73 69 6f 6e 20 33 2e 35 2e 30 20 28 74 61 |ersion 3.5.0 (ta|
00000120 67 73 2f 52 45 4c 45 41 53 45 5f 33 35 30 2f 66 |gs/RELEASE_350/f|
00000130 69 6e 61 6c 20 32 31 37 33 39 34 29 00 00 00 00 |inal 217394)....|
00000140 14 00 00 00 00 00 00 00 03 7a 52 00 01 78 10 01 |.........zR..x..|

La première colonne est l’offset, suivi du code en hexadécimal puis les caractères ASCII. Vous pouvez voir en particulier la chaîne « Hello, world! » qui commence à la ligne 15 et le nom du compilateur « Clang » à la ligne 17.

La commande « objdump » permet de lire le code assemble d’un fichier binaire. On retrouve un code similaire à ce que l’on trouvait dans « main.s » :

clang++ main.cpp -save-temps && objdump -d main.o

affiche :

main.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
 0:   55                     push   %rbp
 1:   48 89 e5               mov    %rsp,%rbp
 4:   48 83 ec 10            sub    $0x10,%rsp
 8:   48 bf 00 00 00 00 00   movabs $0x0,%rdi
 f:   00 00 00 
 12:  48 be 00 00 00 00 00   movabs $0x0,%rsi
 19:  00 00 00 
 1c:  c7 45 fc 00 00 00 00   movl   $0x0,-0x4(%rbp)
 23:  e8 00 00 00 00         callq  28 <main+0x28>
 28:  48 be 00 00 00 00 00   movabs $0x0,%rsi
 2f:  00 00 00

...

L’édition des liens

La dernière étape consiste à lier tous les fichiers binaires ensemble pour créer une application exécutable. Cette étape est réalisée par la commande « ld » (toutes les étapes précédentes étaient réalisées par la commande « cc1 »).

Il est également possible de lire le contenu du fichier exécutable avec la commande « objdump » :

clang++ main.cpp -save-temps && objdump -d a.out

Exécuter le programme

Pour terminer, il serait dommage de ne pas lancer le programme que l’on a compilé (même si c’est un simple hello world). En général, pour compiler avec Clang dans coliru, j’utilise cette ligne de commande :

clang++ -std=c++1z -stdlib=libc++ -Wextra -Wall -pedantic main.cpp
./a.out

ce qui permet d’afficher le fameux message :

Hello, world!

Conclusion

Je n’ai volontairement pas donné toutes les explications sur les commandes présentées dans cet article, ni sur le fonctionnement de la chaîne de compilation. Tout cela sera détaillé dans le cours C++, le principal pour moi ici était de répondre à la question « comment permettre à ceux qui lisent le cours d’avoir quelque chose de concret pour voir et tester ce qu’il se passe lorsque l’on compile ? »

Advertisements

2 commentaires sur « Notes sur le processus de compilation avec GCC/Clang »

  1. « Le code source est donc passé de six lignes dans le « .cpp » à plus de 17 mille lignes dans le « .ii ». »
    –> et après se demande pourquoi inclure des fichiers dans tous les sens augmentent le temps de compilation…

    Article très clair, très efficace. Je garde le lien car il est très utile pour les débutants ou le gens ne comprenant pas vraiment ce qu’il se passe pour passer des sources à un exécutable.

    Pierre / Bktero.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s