TDM n°16 – Grands principes de compilation
Posté le 24/10/2019 00:38
Le Tuto Du Mercredi [TDM] est une idée qui fut proposée par Ne0tux. Le principe est simple : nous écrivons et postons les Mercredis des tutoriels sur l'Utilisation de la calculatrice, le Transfert, les Graphismes, la Programmation, ou encore la Conception de jeu.
Aujourd'hui, on va parler de compilation et pourquoi c'est important.
Niveau ★ ★ ★ ☆ ☆
Mots-clés: Compilation, C, Édition des liens, Makefile
En programmation, la compilation est une étape du développement qui se trouve entre l'écriture du programme et son exécution. Selon les langages et les outils qu'on utilise, elle peut ne pas exister du tout, ou au contraire être un étape cruciale où énormément de choses se passent. Sur Planète Casio, on la rencontre surtout quand on écrit des add-ins en C ou C++, et ses messages d'erreur parfois cryptiques laissent plus d'un développeur amateur perplexe.
Dans ce tutoriel, je vais vous expliquer les grandes lignes du processus de compilation, avec l'exemple d'un add-in. Je parlerai du compilateur GCC, de l'éditeur de liens, des Makefile, et des rôles qu'ils remplissent. Vous verrez que le code C et les fichiers g1a n'ont rien à voir et que la transformation du premier en le second révèle des mécanismes passionants.
Le principe : construire un programme exécutable
Lorsque vous lancez un programme sur votre PC ou calculatrice, c'est le processeur qui exécute le code. Mais le processeur ne sait pas lire ou comprendre le code C ; il parle un langage bien à lui qu'on appelle
assembleur. C'est un langage bas-niveau, peu expressif, et avec lequel il est facile d'écrire des programmes faux et proportionnellement difficile d'écrire des programmes justes.
En plus de ça, le langage assembleur est différent (voire
extrêmement différent) d'un processeur à l'autre, à cause des variations d'architecture. Donc un programme assembleur ne marche vraiment que pour un seul processeur ! Tous ces facteurs ont poussé les informaticiens ont inventé des langages plus simples à utiliser, et plus puissants, comme le Basic et le C. L'idée est de programmer par étapes :
1. On écrit des programmes en C (par exemple). Comme le C est un langage expressif, le code est plus facile à lire et à écrire, et il y a moins de bugs.
2. On traduit ce code vers de l'assembleur pour notre processeur à l'aide d'un traducteur. Si le traducteur fait bien son boulot, on obtient automatiquement un programme assembleur qui fait pareil que notre code C.
3. On donne le programme assembleur au processeur et tout le monde est content.
Le traducteur qui transforme le code C en assembleur s'appelle un
compilateur. C'est un outil indispensable lorsqu'on veut exécuter un programme directement sur le processeur, car généralement on ne veut pas coder en assembleur !
Il y a des compilateurs de tous poils. GCC sait compiler (entre autres) du C et du C++ vers de l'assembleur pour une large gamme d'architectures. LLVM compile vers un langage intermédiaire qu'il recompile ensuite en assembleur. Le compilateur Haskell compile du code Haskell en du code C puis demande à GCC de finir le travail... les possibilités sont nombreuses. Le seul point commun est que ça traduit des langages de programmation.
Le processus complet : assemblage et édition des liens
Le processus complet se fait en fait en plusieurs étapes. L'assembleur est non seulement difficile à utiliser, mais se présente également sous forme binaire. Cela signifie que le code assembleur ne peut pas s'afficher sous forme de texte... ni s'écrire facilement. (>_<)
Avant d'inventer les langages de haut niveau, les informaticiens ont donc commencé par inventer des représentations textuelles pour l'assembleur. C'est exactement le même langage mais représenté sous forme de texte. Notez que le processeur ne comprend pas le texte, que le binaire : et donc il faut traduire.
Le programme qui traduit le langage assembleur textuel en langage assembleur binaire s'appelle un
assembleur. Mais pour éviter les confusions, je vais plutôt dire
programme d'assemblage.
Par facilité, le compilateur C produit de l'assembleur texte. Lorsque le compilateur a fini de travailler, on utilise donc le programme d'assemblage pour retransformer le résultat en code binaire. Le schéma complet ressemble à ça :
Ici, chaque fichier
.c est un fichier source. Chaque fichier
.s correspondant est le code assembleur sous forme textuelle après la compilation. Chaque fichier
.o est le code assembleur binaire associé.
À ce stade, tous les fichiers ont été compilés individuellement, mais il reste encore à les réunir, partager les fonctions et les variables globales, ajouter les bibliothèques, et vérifier que tout y est. Cette étape s'appelle l'édition des liens, et le programme qui la fait s'appelle l'
éditeur de liens (
linker en anglais).
L'édition des liens est un sujet assez compliqué qui mériterait un tutoriel entier à lui tout seul.
En pratique sur la ligne de commande
Prenons un fichier de code C innocent,
example.c. Sous sa forme originale, c'est du texte facile à lire et à comprendre.
% cat example.c
#include <stdio.h>
int main(void)
{
puts("Hello, World!");
return 0;
}
Compilons-le ensemble avec GCC pour obtenir le fichier
example.s contenant une version assembleur textuelle du code.
% gcc -S example.c -o example.s -O3
% cat example.s
.file "example.c"
.text
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "Hello, World!"
.section .text.startup,"ax",@progbits
.p2align 4
.globl main
.type main, @function
main:
subq $8, %rsp
leaq .LC0(%rip), %rdi
call puts@PLT
xorl %eax, %eax
addq $8, %rsp
ret
.size main, .-main
.ident "GCC: (GNU) 9.1.0"
.section .note.GNU-stack,"",@progbits
Le code est déjà bien moins avenant ! Et pourtant il fait la même chose, il appelle la fonction
puts() avec en paramètre un pointeur vers une chaîne de caractère
"Hello, World!".
Maintenant, on peut assembler ça en code objet (assembleur sous forme binaire) à l'aide du programme d'assemblage qui s'appelle
as. Une fois cette étape passée, il n'est plus possible d'afficher directement le fichier car ce n'est plus du texte. À la place, on utilise le programme
objdump qui décode le binaire pour nous.
% as -c example.s -o example.o
% objdump -d example.o
(...)
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8,%rsp
4: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi
b: e8 00 00 00 00 callq 10 <main+0x10>
10: 31 c0 xor %eax,%eax
12: 48 83 c4 08 add $0x8,%rsp
16: c3 retq
Vous voyez qu'on retrouve les mêmes instructions. Cependant le fichier
example.o contient uniquement le binaire décrit dans la colonne du milieu. C'est au bord de l'illisible pour des humains.
On peut finalement appeler l'éditeur de liens. L'éditeur de liens s'appelle
ld mais il est difficile à invoquer sur la ligne de commande, donc on va s'adresser à GCC qui est capable de l'appeler pour nous avec tous les détails corrects. Ensuite on peut lancer le programme comme voulu.
% gcc example.o -o example
% ./example
Hello, World!
Le fichier
example est un fichier ELF (un format de code binaire) et correspond au fichier
game.elf sur mon diagramme. Quand on programme sous Linux, le fichier ELF qui est obtenu après l'édition des liens est le dernier maillon de la chaîne.
Sur calculatrice par contre, il y a encore un peu de travail à faire avant d'obtenir un fichier
game.g1a. Je ne rentre pas dans les détails aujourd'hui car le fichier G1A contient essentiellement la même chose que le fichier ELF.
Recompilation partielle et Makefile
Comme vous pouvez le voir sur mon schéma, les fichiers d'un programme C sont tous compilés inviduellement et réunis seulement à la toute fin.
Supposons que j'ai modifié
main.c et que je veux recompiler mon application. Comme
gui.c et
map.c n'ont pas changé, les fichiers
gui.o et
map.o sont déjà à jour. Il me suffit de recompiler
main.c en
main.o et rappeller l'éditeur de liens pour l'étape finale. Je n'ai donc recompilé qu'un seul des trois fichiers ; ça s'appelle une
recompilation partielle.
Ça peut sembler anodin comme ça, car tout recompiler ne serait pas difficile. Mais des gros projets comme Linux ou Firefox peuvent mettre de précieuses minutes (voire des heures parfois...) à compiler. Il est donc important de ne recompiler que ce qui est nécessaire pour gagner du temps !
Et c'est là que compiler commence à devenir très compliqué. D'abord il y a plusieurs programmes à lancer, ensuite on ne veut les lancer que sur les fichiers qui ont été modifiés depuis la dernière compilation. Et si on en oublie, il n'y aura pas d'erreur mais le programme ne marchera pas...
Comme d'habitude, la solution est de tout automatiser et de laisser l'ordinateur faire.
C'est pour accomplir ce travail que des programmes comme
make ont été inventés. Le job de
make est de compiler des applications pour vous simplifier la vie :
•
make sait appeller automatiquement le compilateur, l'assembleur et l'éditeur de liens. Même si généralement on personnalise les commandes dans un fichier appelé "
Makefile"
•
make s'arrange pour ne recompiler que les fichiers qui ont été modifiés depuis la dernière compilation.
• Et
make fait
plein d'autres choses extrêmement utiles.
Le fichier "
Makefile" contient des instructions pour
make, permettant de personnaliser les commandes de compilation ou carrément de l'utiliser pour autre chose (installer les programmes, générer de la documentation, compiler du LaTeX...).
Comme vous pouvez le voir,
make permet de simplifier un travail relativement compliqué, et donc votre vie en tant que développeur.
Conclusion
La compilation est l'art de
traduire des langages de programmation. Les processeurs ne comprennent que l'assembleur et on veut programmer dans d'autres langages, donc on fait traduire nos programmes vers l'assembleur à notre place.
Le procédé complet de compilation contient plusieurs étapes et se termine par l'
édition des liens qui permet réunir plusieurs fichiers en un seul exécutable.
Comme compiler prend du temps, on aime
recompiler uniquement les parties nécessaires d'un projet pour gagner du temps. Les outils comme
make le font automatiquement et sont extrêmement utiles pour les développeurs.
À la prochaine !
Lire le TDM précédent :
TDM 15- L'utilisation de l'espace graphique en programmation
Consulter l'ensemble des TDM
Fichier joint
Citer : Posté le 24/10/2019 09:25 | #
C'était très intéressant.
Merci j'ai enfin compris le fonctionnement/utilité de make
Citer : Posté le 24/10/2019 09:30 | #
Merci pour ce TDM Lephé' !
Il n'est pas toujours facile de maitriser tous les outils que l'on utilise au quotidien en tant que développeur, mais le makefile est en effet un incontournable.
C'est peut-être parce que c'est une problématique à laquelle je suis confrontée en permanence, mais je pense qu'il est nécessaire de préciser que la compilation (La traduction) se fait pour UN type de processeur en particulier. Il faut donc changer de compilateur si l'on change de cible, ce qui ne remet pas forcément tout le makefile en question.
Je trouve la conclusion très pertinente et synthétique.
J'ai corrigé "difficile à utiliseR"
La Planète Casio est accueillante : n'hésite pas à t'inscrire pour laisser un message ou partager tes créations !
Citer : Posté le 24/10/2019 09:56 | #
Merci pour ces retours !
Merci j'ai enfin compris le fonctionnement/utilité de make
Un jour je rentrerai plus dans les détails je pense. Je ne souhaite pas faire un tutoriel d'écriture de Makefile (il y en a déjà des bons), mais expliquer plus en détail ce qui peut se passer quand on lance make : la fait qu'il utilise la date de modification des fichiers, les appels récursifs, compiler avec plusieurs procos, les règles implicites...
En fait ce TDM est un petit test pour moi, pour voir si ça vaut la peine de faire une mini-série de tutoriels sur la compilation en général. J'aimerais avoir un peu plus de place pour aborder le sujet, et le faire de façon progressive : le premier tutoriel dit tout sans être trop technique (par exemple ce TDM), le second commence à rentrer dans des détails spécifiques, et ensuite ça se précise au fur et à mesure.
J'ai tiqué aussi, j'aurais dû le mettre. J'ai modifié la première partie pour être clair sur la variété des langages assembleur.
Citer : Posté le 01/11/2019 20:33 | #
Merci pour le tutoriel très intéressant!
Citer : Posté le 15/11/2023 23:59 | #
On est bien d'accord que :
4: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi
b: e8 00 00 00 00 callq 10 <main+0x10>
10: 31 c0 xor %eax,%eax
12: 48 83 c4 08 add $0x8,%rsp
16: c3 retq
C'est de la base 16 donc hexadécimale si je me souviens bien et non pas du binaire
Collège fx92+
Aucun projet en cour faut déjà que j'apprenne a faire des add-in et donc a programmer en C un minimum
Citer : Posté le 16/11/2023 00:28 | #
Oui c'est de l'hexa, tout à fait. Le "forme binaire" dans ma phrase fait référence au format du fichier, spécifiquement le fait que c'est des instructions encodées et pas du texte. C'est donc indépendant de la base dans laquelle on affiche ces contenus. Dans la même veine, y'a des termes comme "fichier objet", "(fichier) binaire", "exécutable"... enfin voilà, détails détails.
Citer : Posté le 16/11/2023 17:30 | #
Ah ok
Collège fx92+
Aucun projet en cour faut déjà que j'apprenne a faire des add-in et donc a programmer en C un minimum