Seuls les membres ayant 30 points peuvent parler sur le chat.

Forum Casio - Actualités


Index du Forum » Actualités » TDM n°16 – Grands principes de compilation
LephenixnoirHors ligneAdministrateurPoints: 16017 Défis: 140 Message

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


KikoodxEn ligneMembrePoints: 1447 Défis: 9 Message

Citer : Posté le 24/10/2019 09:25 | #


C'était très intéressant.
Merci j'ai enfin compris le fonctionnement/utilité de make
Bum-bo wants coins

2+2=5
Ne0tuxHors ligneMembre d'honneurPoints: 3287 Défis: 263 Message

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"
Mes principaux jeux : Ice Slider - CloneLab - Arkenstone

La Planète Casio est accueillante : n'hésite pas à t'inscrire pour laisser un message ou partager tes créations !
LephenixnoirHors ligneAdministrateurPoints: 16017 Défis: 140 Message

Citer : Posté le 24/10/2019 09:56 | #


Merci pour ces retours !

Kikoodx a écrit :
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.

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.

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.
Palpatine_78Hors ligneMembrePoints: 72 Défis: 0 Message

Citer : Posté le 01/11/2019 20:33 | #


Merci pour le tutoriel très intéressant!
"ἕν οἶδα ὅτι οὐδὲν οἶδα" Socrate

Planète Casio v42 © créé par Neuronix et Muelsaco 2004 - 2019 | Il y a 56 connectés | Nous contacter | Qui sommes-nous ? | Licences et remerciements

Planète Casio est un site communautaire non affilié à Casio. Toute reproduction de Planète Casio, même partielle, est interdite.
Les programmes et autres publications présentes sur Planète Casio restent la propriété de leurs auteurs et peuvent être soumis à des licences ou copyrights.
CASIO est une marque déposée par CASIO Computer Co., Ltd