Principes des Langages de Programmation La chaine de compilation
John Samuel
CPE Lyon
Année: 2024-2025
Courriel: john.samuel@cpe.fr
1. Prétraitement (Étape 1)
#include
, qui peuvent être des fichiers d’en-tête ou des fragments de code
source.#ifdef
, #ifndef
, #else
et
#endif
.
La sortie du prétraitement est une version modifiée du code source original, qui est ensuite transmise à l’étape suivante.
2. Analyse lexicale (Étape 2)
La sortie de l’analyse lexicale est une liste de jetons, qui est ensuite transmise à l’analyseur syntaxique pour un traitement ultérieur.
3. Analyse syntaxique (Étape 3)
La sortie de l’analyse syntaxique est un AST validé, qui représente la structure syntaxique du programme. Cette étape garantit que le code source respecte les règles de syntaxe du langage.
4. Analyse sémantique (Étape 4)
La sortie de l’analyse sémantique est un AST validé avec des informations de type et de portée. Cette étape garantit que le code source respecte les règles sémantiques du langage.
5. Génération de code intermédiaire (Étape 5)
La sortie de la génération de code intermédiaire est une représentation intermédiaire indépendante de la plate-forme, qui peut être exécutée par un environnement d’exécution. Cette étape garantit que le code source est converti en une forme qui peut être exécutée de manière efficace.
6. Génération de code natif (Étape 6)
La sortie de la génération de code natif est le code exécutable final qui peut être exécuté directement sur la plate-forme cible. Cette étape garantit que le code source est traduit en code machine optimisé pour l’exécution.
Supposons que nous ayons un programme “Hello, World!” simple écrit en C :
#include <stdio.h>
int main() {
printf("Bonjour le monde !!!\n");
return 0;
}
Le processus de compilation se déroulerait comme suit :
#include
pour
inclure le fichier d’en-tête de la bibliothèque standard.La sortie finale est le code exécutable qui peut être exécuté directement sur la plate-forme cible.
Cette étape consiste à traiter les directives de préprocesseur contenues dans le code source, telles que les directives d'inclusion (#include) et de remplacement de macros (#define). Le résultat est généralement un fichier source modifié avec ces directives résolues.
#ifndef PI
#define PI 3.14159
#endif
float aire(
float rayon) {
return(PI * rayon
* rayon);
}
$ gcc -E cercle.c
# 1 "cercle.c"
# 1 ""
# 1 ""
# 31 ""
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "" 2
# 1 "cercle.c"
float aire(float rayon) {
return(3.14 * rayon * rayon);
}
Le résultat affiche le code source après le traitement des directives de préprocesseur. Cela peut inclure des inclusions de fichiers, la résolution de macros et d'autres transformations avant la compilation proprement dite.
Ces étapes montrent comment le code source est transformé en diverses représentations intermédiaires avant d'atteindre le code machine final. Chacune de ces étapes permet une analyse et une optimisation du code à différents niveaux.
$ gcc -v bonjour.c # les étapes importantes
$ gcc -save-temps -v bonjour.c # les fichiers *.i, *.s
$ gcc -fdump-tree-all bonjour.c
$ gcc -fdump-rtl-all bonjour.c
Ce code généré montre comment les opérations mathématiques sont exprimées en AST (Abstract Syntax Tree - arbre de syntaxe abstraite).
$ gcc -fdump-tree-all -c cercle.c
$ cat cercle.c.005t.gimple
aire (float rayon)
{
float D.1914;
_1 = (double) rayon;
_2 = _1 * 3.140000000000000124344978758017532527446746826171875e+0;
_3 = (double) rayon;
_4 = _2 * _3;
D.1914 = (float) _4;
return D.1914;
}
$ gcc -O2 bonjour.c
L'option -S indique au compilateur de produire le code assembleur au lieu de générer un exécutable. Le résultat de cette étape est un fichier avec l'extension .s, qui contient le code assembleur généré.
$ gcc -S bonjour.c
$ cat bonjour.s
int main() {
int num = 2 + 3;
return(0);
}
$ gcc -O0 -S somme.c
L'absence d'optimisation (-O0) signifie que le compilateur n'a pas appliqué d'optimisations particulières, ce qui permet d'obtenir un code assembleur directement équivalent au code C source.
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $5, -4(%rbp)
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
int main() {
int num = 2 + 3;
return(0);
}
$ gcc -O2 -S somme.c
somme.s
main:
.LFB0:
.cfi_startproc
xorl %eax, %eax
ret
.cfi_endproc
Compilation (optimisation: 2) : L'optimisation de niveau 2 a permis au compilateur de reconnaître que l'addition 2 + 3 est constante et que la valeur de retour de la fonction est toujours 0. Par conséquent, le calcul inutile est éliminé, ce qui donne un code assembleur très efficace et court. Cette optimisation est typique des optimisations de constantes que le compilateur peut effectuer pour améliorer les performances du code.
$ gcc bonjour.c
$ file a.out
a.out: ELF 64-bit LSB shared object, x86-64
Cela signifie que le programme compilé est conçu pour être exécuté sur une machine d'architecture 64 bits, spécifiquement une architecture x86-64.
Cette commande indique au compilateur GCC de générer un exécutable 32 bits en utilisant l'architecture i686 (Intel 80386). L'option -m32 spécifie que l'exécutable doit être compilé en tant qu'application 32 bits.
$ gcc -march=i686 -m32 bonjour.c
Cette commande est similaire à la précédente, mais elle génère également le code assembleur (-S) pour le programme.
$ gcc -S -march=i686 -m32 bonjour.c
$ file ./a.out
./a.out: ELF 32-bit LSB shared object, Intel 80386
Cette commande compile le fichier source "client.c" en un fichier objet "client.o". L'option -c indique au compilateur de générer uniquement le fichier objet sans produire d'exécutable. Cela permet de diviser le processus de compilation en deux étapes.
$ gcc -c client.c
$ gcc -c color.c
Cette commande prend les fichiers objets "color.o" et "client.o" et les compile en un exécutable nommé "color". Le compilateur lie les fichiers objets pour créer l'exécutable final.
$ gcc -o color color.o client.o
Cette approche est couramment utilisée pour séparer la compilation en plusieurs étapes, ce qui peut être utile dans des projets plus importants où différentes parties du programme sont gérées séparément avant d'être liées ensemble pour former l'exécutable final.
$ gcc -c client.c color.c
$ gcc -o client client.o color.o
$ vim client.c # Modification du fichier
$ gcc -c client.c
$ vim client.c # Modification du fichier
$ gcc -c client.c
$ gcc -o client client.o color.o
L'objectif de cette séquence de commandes est de compiler des fichiers source en fichiers objet, puis de recompiler le programme avec des modifications apportées au fichier source "client.c". Cela permet de mettre à jour le programme en reflétant les changements apportés au code source sans avoir à recompiler entièrement toutes les sources. Cela peut accélérer le processus de développement en évitant de recompiler inutilement les parties du programme qui n'ont pas changé.