Principes des Langages de Programmation
La chaine de compilation

John Samuel
CPE Lyon

Année: 2024-2025
Courriel: john.samuel@cpe.fr
Creative Commons License

3. Principes des Langages de Programmation : Objectifs

Objectifs

3.1. La chaine de compilation

6 étapes de la chaine de compilation

3.1. La chaine de compilation

6 étapes de la chaine de compilation

1. Prétraitement (Étape 1)

La sortie du prétraitement est une version modifiée du code source original, qui est ensuite transmise à l’étape suivante.

3.1. La chaine de compilation

6 étapes de la chaine de compilation

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.1. La chaine de compilation

6 étapes de la chaine de compilation

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.

3.1. La chaine de compilation

6 étapes de la chaine de compilation

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.

3.1. La chaine de compilation

6 étapes de la chaine de compilation

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.

3.1. La chaine de compilation

6 étapes de la chaine de compilation

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.

3.1. La chaine de compilation

6 étapes de la chaine de compilation

Supposons que nous ayons un programme “Hello, World!” simple écrit en C :

#include <stdio.h>

int main() {
    printf("Bonjour le monde !!!\n");
    return 0;
}

3.1. La chaine de compilation

6 étapes de la chaine de compilation

Le processus de compilation se déroulerait comme suit :

  1. Prétraitement : Le préprocesseur développe la directive #include pour inclure le fichier d’en-tête de la bibliothèque standard.
  2. Analyse lexicale : L’analyseur lexical divise le code source en jetons individuels, tels que des mots-clés, des identificateurs, des littéraux et des symboles.
  3. Analyse syntaxique : L’analyseur syntaxique construit un arbre syntaxique abstrait (AST) à partir du flux de jetons, qui représente la structure syntaxique du programme.

3.1. La chaine de compilation

6 étapes de la chaine de compilation

  1. Analyse sémantique : L’analyseur sémantique vérifie que les types des variables et des expressions correspondent aux types attendus et résout les problèmes de portée.
  2. Génération de code intermédiaire : Le générateur de code intermédiaire traduit l’AST validé en bytecode ou en arbres syntaxiques abstraits.
  3. Génération de code natif : Le générateur de code natif applique des optimisations et génère des instructions spécifiques à la machine à partir de la représentation intermédiaire.

La sortie finale est le code exécutable qui peut être exécuté directement sur la plate-forme cible.

3.1. La chaine de compilation

1. Préprocesseur

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.

cercle.c

#ifndef PI
#define PI 3.14159
#endif

float aire( float rayon) {
 return(PI * rayon * rayon);
}

3.1. La chaine de compilation

1. Préprocesseur

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

3.1. La chaine de compilation

2. Langage intermédiaire

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

3.1. La chaine de compilation

2. Langage intermédiaire

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;
}

3.1. La chaine de compilation

3. Optimisation de code

$ gcc -O2 bonjour.c

3.1. La chaine de compilation

4. Génération de code natif

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

3.1. La chaine de compilation

4. Génération de code natif et l'optimisation de code

int main() {
  int num = 2 + 3;
  return(0);
}

Compilation (Pas d'optimisations)

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

3.1. La chaine de compilation

4. Génération de code natif et l'optimisation de code

somme.s

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

3.1. La chaine de compilation

4. Génération de code natif et l'optimisation de code

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.

3.1. La chaine de compilation

4. Génération de code

sur une machine d'architecture 64 bits

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

3.1. La chaine de compilation

4. Génération de code

sur une machine d'architecture 64 bits

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

3.1. La chaine de compilation

5. Code objet

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.

3.1. La chaine de compilation

5. Code objet (Modifications et recompilation)

$ 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é.

3. Principes des Langages de Programmation : Références

Références

Crédits d'images