L’ordinateur et son fonctionnement
Leçon 6 - Comment fonctionne un compilateur ?
Quand tu écris un programme, tu te sers de mots, de chiffres et de signes que tu comprends. Mais au fond de l’ordinateur, le processeur, lui, ne comprend qu’une seule chose...
Comment fonctionne un compilateur ?
jusqu’aux instructions que la machine exécute
Quand tu écris un programme, tu te sers de mots, de chiffres et de signes que tu comprends. Mais au fond de l’ordinateur, le processeur, lui, ne comprend qu’une seule chose : des suites de 0 et de 1. Entre ces deux mondes, il faut un traducteur. Il s’appelle un compilateur, et c’est l’une des inventions les plus astucieuses de toute l’informatique.
Pour bien le comprendre, nous allons suivre un seul et même petit programme, écrit dans un langage très répandu, le langage C. Nous l’accompagnerons d’un bout à l’autre de son voyage, comme nous suivrions une lettre depuis la boîte aux lettres jusqu’à son destinataire. À chaque chapitre, nous ouvrirons une nouvelle porte de la machine à traduire.
Pourquoi traduire ? Du langage humain au langage machine
Imagine que tu veuilles donner des instructions à quelqu’un qui ne parle pas ta langue. Tu as deux solutions : apprendre la sienne toi-même, ce qui est très difficile, ou bien faire appel à un traducteur. En informatique, c’est le même problème. Le processeur, le « cerveau » de la machine, ne sait lire que le langage machine : des nombres écrits en binaire, où chacun commande une action minuscule (additionner, déplacer une valeur, comparer…). C’est précis, mais pénible à écrire pour un être humain.
Nous, nous préférons écrire dans un langage proche de notre façon de penser, avec des mots et des formules. Le langage C en fait partie. Le compilateur est le programme qui fait le pont entre les deux : il prend notre texte et le transforme en quelque chose que la machine peut exécuter, sans en changer le sens.
Un compilateur est un programme qui traduit un programme écrit dans un langage de programmation (le langage source, comme le C) en un programme équivalent écrit dans un langage que la machine peut exécuter (le langage machine). « Équivalent » signifie que le programme traduit fait le même travail que le programme de départ.
La carte du voyage
Pour le langage C, la traduction n’est pas faite par un seul outil, mais par une petite équipe d’outils qui se passent le travail, chacun ayant une tâche bien précise. C’est cette équipe que l’on appelle la chaîne de compilation :
(.c)
La chaîne de compilation du langage C. Chaque outil reçoit le travail du précédent et le transmet au suivant.
Dans les chapitres suivants, nous regarderons à la loupe ce qui se passe à l’intérieur de la case « Compilateur », car c’est là que se trouvent les étapes les plus surprenantes. Mais commençons par le tout premier ouvrier de la chaîne, celui qui prépare le terrain.
Le préprocesseur : préparer le texte
Avant même que la véritable traduction ne commence, un outil appelé préprocesseur nettoie et prépare le texte du programme. Son travail est purement un travail sur le texte : il ne comprend pas ce que le programme veut dire, il se contente de le réécrire. Il fait surtout trois choses : il insère le contenu des fichiers demandés par une ligne commençant par #include, il remplace les raccourcis définis par #define, et il retire les commentaires (les remarques que le programmeur a écrites pour lui-même et qui n’ont pas d’effet sur le calcul).
Dans notre programme, la ligne #include <stdio.h> demande d’insérer un fichier nommé stdio.h, qui contient des informations sur les outils d’affichage, comme printf. Une fois ce travail terminé, nous obtenons un texte source « préparé », prêt pour la suite.
Au début des années 1950, l’idée qu’une machine puisse « se traduire ses propres instructions » semblait farfelue à beaucoup. Grace Hopper (1906-1992), mathématicienne et officier de la marine américaine, n’y croyait pas seulement : elle l’a réalisée. Vers 1952, sur l’ordinateur UNIVAC, elle met au point un programme appelé A-0, capable de transformer des instructions plus lisibles en instructions exécutables. C’est l’une des toutes premières étapes vers les compilateurs modernes, et c’est aussi à Grace Hopper que l’on doit la diffusion du mot « compilateur ».
Elle racontait que ses collègues lui répétaient que les ordinateurs « ne savaient faire que de l’arithmétique ». Les tout premiers compilateurs étaient en effet très simples, et l’idée a mis plusieurs années à mûrir pour devenir ce que nous connaissons aujourd’hui. Mais le pas décisif était franchi : une machine pouvait aider à se programmer elle-même.
Voici une idée qui ressemble à un casse-tête : un compilateur pour un langage peut lui-même être écrit… dans ce langage. Une grande partie des compilateurs du langage C est, par exemple, écrite en C. On appelle cela l’amorçage (en anglais, bootstrapping).
Comment est-ce possible, puisqu’il faudrait déjà un compilateur pour compiler le premier ? La solution vient par étapes : nous écrivons d’abord une version très simple du compilateur dans un autre langage déjà disponible ; cette première version sert ensuite à en construire une plus complète, capable à son tour de se compiler elle-même. Un peu comme une échelle dont chaque barreau se construit en montant sur le précédent.
Voici le petit programme que nous suivrons jusqu’au bout. Il additionne deux nombres et affiche le résultat :
#include <stdio.h>
int main(void) {
int a = 2;
int b = 3;
int somme = a + b;
printf("%d\n", somme);
return 0;
}
Nous nous concentrerons surtout sur la ligne du milieu, int somme = a + b;, qui calcule la somme. C’est elle qui traversera toutes les étapes de la traduction.
Le processeur ne comprend que le langage machine, fait de nombres binaires. Nous écrivons dans un langage plus lisible, comme le C. Le compilateur traduit l’un dans l’autre sans changer le sens. Pour le langage C, plusieurs outils se relaient : le préprocesseur prépare le texte, puis le compilateur, l’assembleur et l’éditeur de liens prennent le relais. Dans les prochains chapitres, nous entrerons dans le compilateur lui-même.
L’analyse lexicale : découper en mots
Le texte est préparé : la véritable traduction peut commencer. Mais pour le compilateur, le programme n’est encore qu’une longue suite de caractères collés les uns aux autres, comme une phrase écrite sans séparation. Son premier geste consiste donc à découper cette suite en petits morceaux qui ont un sens pour le langage. Ces morceaux portent un nom : ce sont les unités lexicales, ou jetons.
L’analyse lexicale est l’étape qui lit le texte du programme caractère par caractère et le découpe en unités lexicales (ou jetons) : les plus petits éléments porteurs de sens pour le langage. On y trouve les mots-clés (des mots réservés par le langage, comme int ou return), les identificateurs (les noms choisis par le programmeur, comme somme), les nombres, les opérateurs (comme = ou +) et les signes de ponctuation (comme ; ou les parenthèses).
Notre ligne, jeton par jeton
Reprenons l’instruction int somme = a + b;. L’analyse lexicale la transforme en une suite de jetons bien identifiés :
| Jeton | Nature | Rôle |
|---|---|---|
int | Mot-clé | Annonce un nombre entier |
somme | Identificateur | Nom d’une variable |
= | Opérateur | Affectation (ranger une valeur dans la variable) |
a | Identificateur | Nom d’une variable |
+ | Opérateur | Addition |
b | Identificateur | Nom d’une variable |
; | Ponctuation | Marque la fin de l’instruction |
Le voyage de notre ligne ressemble donc à ceci : d’un côté une suite de caractères, de l’autre une série de jetons rangés.
À gauche, le flux de caractères ; à droite, le flux de jetons identifiés.
Une remarque importante pour la suite : à ce stade, le compilateur ne se demande pas encore si l’instruction est correcte ni si elle a un sens. Il ne fait que reconnaître les mots, comme tu identifierais ceux d’une phrase dans une langue étrangère sans encore comprendre ce qu’elle raconte.
Le mot « lexique » vient du grec ancien lexis, qui signifie « le mot, la parole ». Ce n’est pas un hasard : pendant l’analyse lexicale, le compilateur accomplit littéralement un travail sur les mots. Il parcourt le texte du programme et en isole chaque élément, un à un, comme tu découperais une longue phrase dont les mots seraient collés les uns aux autres.
Et justement, parlons des espaces, car ils réservent une surprise. En langage C, ceux situés entre les éléments du code ne comptent pas par leur nombre : qu’on en place un seul ou une dizaine, le découpage donne le même résultat, et a = b + c revient au même que a=b+c. Une exception toutefois : à l’intérieur des guillemets, chaque espace compte, car il fait alors partie du texte que le programme affichera.
En Python, c’est une tout autre histoire. Les espaces placés en début de ligne, l’indentation, ont une réelle importance : ils signalent quelles instructions forment un même groupe. Les déplacer change le sens du programme. Deux langages, donc, qui ne traitent pas les espaces de la même façon.
La première étape à l’intérieur du compilateur est l’analyse lexicale. Elle lit le texte caractère par caractère et le découpe en jetons : mots-clés, identificateurs, nombres, opérateurs et ponctuation. À ce moment, le compilateur reconnaît les mots, mais n’a pas encore vérifié la grammaire ni le sens de la phrase. Ce sera le travail des deux chapitres suivants.
L’analyse syntaxique : vérifier la grammaire
Maintenant que les jetons sont identifiés, une nouvelle question se pose : sont-ils placés dans le bon ordre ? En français, « le chat dort » est une phrase correcte, alors que « dort chat le » ne l’est pas, même si tous les mots sont bons. Les langages de programmation ont eux aussi leurs règles d’agencement, leur grammaire. L’étape qui les vérifie s’appelle l’analyse syntaxique.
L’analyse syntaxique vérifie que la suite des jetons respecte les règles de grammaire du langage. Lorsque c’est le cas, elle organise les jetons en un arbre syntaxique : une représentation en forme d’arbre qui montre comment les éléments dépendent les uns des autres, et dans quel ordre les opérations doivent être faites.
L’arbre de notre instruction
Que veut dire somme = a + b ? Cela signifie : « calcule d’abord a + b, puis range le résultat dans somme ». L’arbre syntaxique capture cette structure :
En bleu, les opérations (= et +) ; en vert, les variables. Les deux numéros en orange indiquent l’ordre des calculs.
Au sommet se trouve l’opération principale, l’affectation = : elle relie ce qui reçoit le résultat (somme, à gauche) à ce qui le produit (l’addition a + b, à droite).
Pour lire cet arbre, nous partons du bas et nous remontons :
- numéro 1 : nous calculons d’abord l’addition
a + b, tout en bas de l’arbre ; - numéro 2 : nous rangeons ensuite ce résultat dans
somme, au sommet.
Voilà ce que décrit l’instruction somme = a + b : calculer la somme, puis l’affecter à la variable. Cette structure sera précieuse pour la traduction qui suit.
Pour vérifier une grammaire, encore faut-il l’avoir écrite quelque part, sans la moindre ambiguïté. C’est l’apport de John Backus (1924-2007). Chez IBM, il dirige l’équipe qui crée FORTRAN, le premier langage de programmation de haut niveau à connaître un large succès (son compilateur est déployé en 1957).
Quelques années plus tard, pour décrire avec précision la grammaire d’un autre langage, ALGOL, il met au point vers 1959 une notation restée célèbre : la forme de Backus-Naur, souvent abrégée BNF. Elle permet d’écrire les règles d’une grammaire de manière rigoureuse, sans le moindre flou. Perfectionnée avec le chercheur Peter Naur, cette notation est encore employée aujourd’hui pour définir les langages de programmation. L’analyse syntaxique a justement besoin de telles règles, claires et nettes, pour faire son travail.
Avec une poignée de règles de grammaire, il est possible de décrire un nombre infini de programmes corrects. C’est le même prodige que pour les langues humaines : les quelques règles du français te permettent de comprendre et de fabriquer des phrases que personne n’avait encore prononcées avant toi. Une grammaire bien écrite n’est pas une liste de phrases autorisées, mais un petit jeu de règles qui en engendre une infinité.
Que se passe-t-il si l’on écrit : int somme = a + ; ? L’analyse lexicale reconnaît pourtant tous les jetons. Mais l’addition + réclame deux valeurs et n’en reçoit qu’une : à droite, il manque un nombre ou une variable. Le compilateur ne peut donc pas construire l’arbre : il s’arrête et affiche une erreur de syntaxe. C’est la fameuse erreur que rencontrent tous les programmeurs lorsqu’ils oublient une parenthèse, un point-virgule ou un morceau de calcul.
L’analyse syntaxique vérifie que les jetons respectent la grammaire du langage et les organise en un arbre syntaxique, qui montre la structure de l’instruction et l’ordre des opérations. Si la grammaire est violée, le compilateur signale une erreur de syntaxe et refuse de continuer. La forme de Backus-Naur, créée par John Backus, sert précisément à écrire ces règles de grammaire sans ambiguïté.
L’analyse sémantique : est-ce que cela a un sens ?
Une phrase peut être parfaitement bien construite et pourtant ne rien vouloir dire. Le compilateur le sait, et c’est pourquoi, après avoir vérifié la grammaire, il pose une autre question, plus profonde : cette instruction a-t-elle un sens ? Cette étape s’appelle l’analyse sémantique, d’après le mot « sémantique », qui a justement trait à la signification.
L’analyse sémantique vérifie que le programme a un sens cohérent : que chaque variable utilisée a bien été déclarée auparavant, et que les opérations s’appliquent à des valeurs compatibles, car elles ne se combinent pas toutes entre elles. Pour cela, le compilateur tient à jour une table des symboles, une sorte de répertoire où il note chaque nom déclaré et son type (par exemple : nombre entier, nombre à virgule, texte…).
La table des symboles de notre programme
Dans notre fil rouge, les trois variables ont été déclarées avec le mot-clé int, qui annonce un nombre entier. Le compilateur les inscrit donc dans sa table :
| Nom | Type | Ce que le compilateur en sait |
|---|---|---|
a | entier (int) | Variable, vaut 2 |
b | entier (int) | Variable, vaut 3 |
somme | entier (int) | Variable qui recevra le résultat |
Grâce à cette table, le compilateur peut vérifier que a + b a bien un sens : additionner deux entiers donne un entier, que nous rangeons dans une variable elle aussi entière. Tout est cohérent. Si l’une de ces conditions n’était pas remplie, il s’arrêterait.
En 1957, le linguiste Noam Chomsky propose une phrase devenue célèbre dans son livre Syntactic Structures : « Colorless green ideas sleep furiously », que l’on peut traduire par « D’incolores idées vertes dorment furieusement ». Lis-la attentivement : elle est grammaticalement parfaite. Il y a un sujet, des adjectifs bien placés, un verbe, un adverbe. Et pourtant, elle ne veut rien dire : des idées ne sont ni vertes ni incolores, et elles ne dorment pas.
Chomsky voulait montrer que la grammaire (la syntaxe) et le sens (la sémantique) sont deux choses différentes : une phrase peut être correcte sur l’une et fausse sur l’autre. C’est précisément la frontière entre le chapitre précédent et celui-ci. Un compilateur peut accepter la grammaire d’une instruction, puis refuser cette même instruction parce qu’elle n’a pas de sens.
Première sorte, une variable inconnue : somme = a + c;. Le compilateur cherche c dans sa table des symboles. Il y trouve a et somme, mais pas c : il signale donc une variable inconnue.
Seconde sorte, un type incompatible : somme = a + "trois";. Cette fois, les trois variables sont déclarées et la grammaire est correcte ; mais additionner un nombre entier et un texte n’a pas de sens. Le compilateur signale une erreur de type.
Dans les deux cas, le compilateur refuse l’instruction avant même de construire le programme.
Le type d’une variable décide aussi de la taille des nombres qu’elle peut contenir, comme une boîte d’une certaine taille. Et lorsqu’un nombre est trop grand pour sa boîte, les difficultés commencent. Le 4 juin 1996, la fusée européenne Ariane 5 explose une trentaine de secondes seulement après son décollage, lors de son tout premier vol. La cause ? Un nombre décrivant la vitesse a été rangé dans une « boîte » trop petite pour lui : un type qui ne pouvait pas dépasser 32 767. La valeur réelle était plus grande, et le programme a échoué.
Le plus instructif est que ce programme avait parfaitement fonctionné sur la fusée précédente, Ariane 4, qui volait moins vite ; sur Ariane 5, plus rapide, le nombre est devenu trop grand. À retenir : cette erreur s’est produite pendant le vol, alors que le programme s’exécutait, et non lors de la compilation. C’est précisément pour éviter ce genre de catastrophe que les programmeurs choisissent les types avec le plus grand soin, bien avant de lancer leur programme.
L’analyse sémantique vérifie que le programme a un sens : variables bien déclarées, opérations appliquées à des valeurs compatibles. Elle s’appuie sur une table des symboles qui mémorise chaque nom et son type. Une instruction grammaticalement correcte peut être refusée à cette étape si elle n’a pas de sens. Et le choix des types n’est pas une simple formalité : il peut, comme pour Ariane 5, avoir des conséquences spectaculaires.
Le code intermédiaire et l’optimisation : traduire, puis améliorer
Le programme a été découpé, sa grammaire vérifiée, son sens contrôlé. Le compilateur peut enfin commencer à traduire. Mais il ne se jette pas tout de suite sur le langage machine : il passe d’abord par une étape intermédiaire, une sorte de brouillon de travail plus simple à manipuler, où il peut aussi améliorer le programme avant de le finaliser.
Le code intermédiaire est une version simplifiée du programme, qui ne dépend pas encore d’un processeur particulier. Comme il est plus facile à transformer, le compilateur en profite pour faire de l’optimisation : il modifie le code pour qu’il accomplisse le même travail plus vite ou avec moins d’instructions, sans en changer le résultat.
Deux améliorations sur notre programme
La première s’appelle le calcul des constantes à l’avance. Dans notre programme, a vaut 2 et b vaut 3. Le compilateur s’en aperçoit : pourquoi laisser la machine calculer 2 + 3 à chaque exécution, alors que le résultat, 5, est déjà connu ? Lorsque l’optimisation est activée, il remplace donc le calcul par sa réponse.
| Avant optimisation | Après optimisation |
|---|---|
Calculer a + b, c’est-à-dire 2 + 3 | Écrire directement 5 |
Ranger ce résultat dans somme | Ranger 5 dans somme |
La seconde amélioration est la suppression de l’inutile : si le compilateur remarque qu’une valeur est calculée mais non utilisée par la suite, il peut tout simplement effacer ce calcul. Le programme contient alors moins d’instructions et s’exécute plus vite, pour un résultat visible identique.
Quand l’équipe de John Backus présente FORTRAN en 1957, beaucoup de programmeurs sont méfiants. Ils écrivaient jusque-là leurs programmes à la main, instruction par instruction, et ils étaient persuadés qu’une traduction automatique ne pourrait pas produire un code aussi rapide que le leur. Pour les convaincre, l’équipe consacre un travail énorme à l’optimisation : FORTRAN devient l’un des premiers compilateurs capables de produire un code machine d’une qualité comparable à celui d’un expert humain.
Ce pari a été décisif. C’est en grande partie parce qu’il produisait un code rapide que FORTRAN a fini par être adopté : les programmeurs ont accepté de confier la traduction à la machine, voyant qu’elle ne les desservait pas. L’optimisation n’était pas un détail : c’était la clé de la confiance.
Il est possible de demander au compilateur d’optimiser plus ou moins. Sans optimisation, il traduit fidèlement, instruction après instruction, ce qui est pratique pour chercher une erreur. Avec un niveau d’optimisation élevé, il prend le temps de chercher toutes sortes d’améliorations, et le programme obtenu peut être nettement plus rapide. C’est d’ailleurs pour cette raison que, sans optimisation, le calcul 2 + 3 de notre exemple ne serait pas forcément remplacé par 5 : c’est une amélioration que le compilateur ne fait que s’il en a reçu la consigne.
Avant de produire du langage machine, le compilateur passe par un code intermédiaire, plus simple et indépendant du processeur. Il en profite pour optimiser : calculer à l’avance ce qui peut l’être, supprimer ce qui ne sert à rien, le tout sans changer le résultat. L’histoire de FORTRAN montre à quel point cette qualité de traduction a compté pour faire accepter les compilateurs.
Du code machine à l’exécutable : assembleur et éditeur de liens
Nous voici à la dernière ligne droite. Le compilateur a préparé un code propre et amélioré ; il faut maintenant le transformer en un véritable fichier que l’on peut lancer. Trois opérations s’enchaînent pour cela.
Première opération : le code assembleur
Le compilateur produit d’abord du code assembleur. C’est un langage très proche de celui du processeur, où chaque ligne décrit une action minuscule : charger une valeur, l’additionner, ranger un résultat. Pour aller vite, le processeur dispose de quelques petites cases mémoire ultra-rapides appelées registres (comme nous l’avons vu en étudiant le processeur), et le code assembleur indique précisément quel registre utiliser à chaque instant.
Voici l’idée de ce que devient somme = a + b, présentée en français et en version très simplifiée (le véritable assembleur est plus détaillé et dépend du modèle de processeur) :
charger 2 dans un registre
charger 3 dans un autre registre
additionner les deux registres
ranger le résultat dans somme
Et si l’optimisation du chapitre 5 a déjà calculé le résultat, ces lignes se réduisent à une seule : « ranger 5 dans somme ». Le travail prévu d’avance allège jusqu’au code assembleur.
Deuxième opération : l’assembleur
Un programme appelé assembleur traduit ensuite ce code en véritable code machine : des nombres binaires que le processeur exécute. Le résultat est un fichier objet. Attention : à ce stade, les appels à des fonctions venues d’ailleurs, comme printf, ne sont pas encore complétés ; l’assembleur laisse une « case vide » à leur emplacement, à remplir plus tard.
Troisième opération : l’éditeur de liens
C’est le rôle de l’éditeur de liens. Il rassemble notre fichier objet avec le code, déjà tout prêt, des fonctions que nous avons utilisées sans les écrire nous-mêmes, comme printf. Ce code prêt à l’emploi est rangé dans des bibliothèques, fournies avec le langage. L’éditeur de liens va y chercher ce qu’il manque, comble les « cases vides », et produit enfin un seul fichier : l’exécutable, que le système d’exploitation peut lancer.
Le code machine est le langage du processeur : des suites de nombres binaires, où chaque nombre représente une instruction ou une donnée. Un fichier objet contient ce code machine pour une partie du programme, avec les appels externes encore à compléter. Une bibliothèque est une réserve de code déjà écrit et prêt à servir. L’éditeur de liens assemble fichiers objets et bibliothèques pour produire l’exécutable, le fichier final que l’on peut lancer.
Le voyage complet des fichiers
.c.s.o (binaire)Les noms de fichiers (.c, .s, .o) sont ceux qu’utilisent réellement les outils du langage C.
Tu n’as pas écrit le code de printf, et pourtant il affiche bien le résultat. Comment ? Parce que ce code existe déjà, tout prêt, dans une bibliothèque livrée avec le langage. L’éditeur de liens vient l’y chercher et l’ajoute à ton programme au tout dernier moment. C’est un gain énorme : des milliers de programmes peuvent réutiliser le même printf sans que personne n’ait à le réécrire.
L’exécutable est né : nous pouvons le lancer. Mais un programme qui démarre n’est pas forcément un programme sans défaut. Le 9 septembre 1947, l’équipe travaillant sur l’ordinateur Mark II de l’université Harvard cherche la cause d’une panne. Dans l’un des relais de la machine, elle trouve… un véritable papillon de nuit, coincé entre les contacts. Quelqu’un le retire et le colle dans le carnet de bord avec cette note : « First actual case of bug being found », c’est-à-dire « premier cas avéré d’un insecte trouvé ».
Une précision s’impose, car l’histoire est souvent déformée. Le mot anglais « bug », qui désigne un défaut technique, existait déjà bien avant : il apparaît par exemple sous la plume de l’inventeur Thomas Edison dès les années 1870. Grace Hopper, présente dans l’équipe, a beaucoup raconté cette anecdote au fil de ses conférences, ce qui l’a rendue célèbre ; mais ce n’est pas elle qui a trouvé l’insecte, et le carnet n’était probablement pas le sien. Aujourd’hui encore, « déboguer » un programme, c’est y chercher et corriger les erreurs qui n’apparaissent qu’à l’usage.
Pour finir la traduction, trois outils se relaient : le compilateur produit du code assembleur (.s), l’assembleur le transforme en code machine binaire rangé dans un fichier objet (.o), puis l’éditeur de liens rassemble ce fichier avec les bibliothèques pour produire l’exécutable. C’est l’éditeur de liens qui va chercher le code tout prêt de fonctions comme printf. Une fois lancé, le programme peut encore contenir des erreurs : les fameux « bugs ».
Compilateur ou interprète ?
Tout au long de ce cours, nous avons traduit notre programme entièrement, à l’avance, pour obtenir un exécutable. C’est la voie du compilateur, celle du langage C. Mais ce n’est pas la seule façon de faire fonctionner un programme. Il existe une autre approche, qui ne fabrique pas de fichier exécutable séparé : l’interprète.
Un interprète est un programme qui lit un programme source et l’exécute directement, instruction par instruction, sans produire au préalable un fichier exécutable séparé. Le compilateur, lui, traduit tout d’avance en un exécutable, que l’on peut ensuite lancer autant de fois que l’on veut.
Pour comparer, une image : le compilateur ressemble à un traducteur qui prend un livre entier et en publie une traduction complète, une fois pour toutes. L’interprète ressemble à un interprète humain qui, lui, traduit à voix haute, phrase après phrase, au fur et à mesure que l’orateur parle.
| Compilateur (ex. : C) | Interprète (ex. : Python, en usage courant) | |
|---|---|---|
| Quand la traduction a lieu | Une fois, avant l’exécution | À chaque exécution, au fil des lignes |
| Résultat produit | Un fichier exécutable | Pas de fichier exécutable séparé |
| Vitesse d’exécution | En général plus rapide | En général plus lente |
| Détection des erreurs | Beaucoup sont repérées avant de lancer | Souvent au moment où la ligne est atteinte |
| Pour tester rapidement | Il faut recompiler après chaque changement | On peut essayer immédiatement |
Écrit en C, notre programme est compilé une seule fois en un exécutable, qui calcule et affiche 5 à chaque lancement, très vite. Le même calcul écrit en Python serait, lui, pris en charge par l’interprète Python, qui le lirait et l’exécuterait ligne par ligne, à chaque lancement. Le résultat affiché est le même ; c’est le chemin pour y parvenir qui diffère.
Beaucoup de systèmes modernes mélangent les deux approches. Le langage Java, par exemple, est d’abord compilé non pas en code machine, mais en un code intermédiaire appelé bytecode (des octets). Ce bytecode est ensuite exécuté par un programme spécial, la machine virtuelle (la JVM), présent sur chaque ordinateur. L’avantage est élégant : le même programme Java peut fonctionner sur des machines très différentes, car c’est la machine virtuelle de chacune qui se charge de la dernière étape. Nous retrouvons ici l’idée de code intermédiaire rencontrée au chapitre 5.
Un compilateur traduit tout d’avance et produit un exécutable ; un interprète lit et exécute le programme ligne par ligne, sans fichier exécutable séparé. Le compilé est en général plus rapide, l’interprété plus souple pour tester. De nombreux langages, comme Java, combinent les deux en passant par un code intermédiaire exécuté par une machine virtuelle.
Le voyage complet, d’un seul coup d’œil
Récapitulons tout le chemin parcouru par notre petit programme, depuis le texte que nous avons écrit jusqu’à l’exécutable que la machine peut lancer :
.c.s.oLes étapes en vert manipulent des fichiers ; les étapes intérieures au compilateur transforment le programme en mémoire.
Ce qu’il faut retenir, au fond, est simple et magnifique : un compilateur ne se contente pas de remplacer des mots par d’autres. Il comprend la structure de ton programme, en vérifie la grammaire et le sens, l’améliore, puis le traduit jusqu’au langage de la machine, sans en trahir l’intention. Chaque fois que tu lances un programme, toute cette mécanique invisible a déjà fait son travail.
Lexique des termes importants
- Compilateur
- Programme qui traduit un programme source en un programme équivalent dans un langage exécutable par la machine.
- Langage source
- Le langage dans lequel le programmeur écrit, comme le C. Lisible par un être humain.
- Langage machine (code machine)
- Le langage du processeur : des suites de nombres binaires, où chaque nombre commande une action.
- Binaire
- Façon d’écrire les nombres avec seulement deux chiffres, 0 et 1, la seule que le processeur sache lire.
- Préprocesseur
- Outil qui prépare le texte du programme avant la traduction : insertion des fichiers demandés, remplacements, retrait des commentaires.
- Jeton (unité lexicale)
- Plus petit élément porteur de sens repéré dans le texte : mot-clé, identificateur, nombre, opérateur ou ponctuation.
- Mot-clé
- Mot réservé par le langage, qui a une signification fixée (par exemple
intoureturn). - Identificateur
- Nom choisi par le programmeur pour désigner une variable ou une fonction (par exemple
somme). - Analyse lexicale
- Étape qui découpe le texte en jetons.
- Analyse syntaxique
- Étape qui vérifie la grammaire et construit l’arbre syntaxique.
- Grammaire
- Ensemble des règles qui disent comment agencer correctement les jetons d’un langage.
- Arbre syntaxique
- Représentation en forme d’arbre de la structure d’une instruction et de l’ordre de ses opérations.
- Forme de Backus-Naur (BNF)
- Notation qui permet d’écrire une grammaire sans ambiguïté. Créée par John Backus, perfectionnée avec Peter Naur.
- Analyse sémantique
- Étape qui vérifie que le programme a un sens : variables déclarées, opérations compatibles.
- Type
- Nature d’une valeur (nombre entier, nombre à virgule, texte…), qui décide de ce qu’il est possible d’en faire et de la taille des nombres acceptés.
- Table des symboles
- Répertoire tenu par le compilateur, où sont notés chaque nom déclaré et son type.
- Code intermédiaire
- Version simplifiée du programme, indépendante du processeur, plus facile à transformer.
- Optimisation
- Transformation du code pour le rendre plus rapide ou plus court, sans changer son résultat.
- Registre
- Petite case mémoire très rapide située dans le processeur.
- Assembleur
- Langage très proche du processeur ; aussi le nom du programme qui le traduit en code machine.
- Fichier objet
- Fichier contenant le code machine d’une partie du programme, avec les appels externes encore à compléter.
- Bibliothèque
- Réserve de code déjà écrit et prêt à servir (par exemple le code de
printf). - Éditeur de liens
- Outil qui rassemble fichiers objets et bibliothèques pour produire l’exécutable.
- Exécutable
- Fichier final, prêt à être lancé par le système.
- Interprète
- Programme qui lit et exécute un programme source ligne par ligne, sans fabriquer d’exécutable séparé.
- Bytecode
- Code intermédiaire (utilisé par exemple par Java), exécuté par une machine virtuelle.
- Machine virtuelle
- Programme qui exécute un bytecode, permettant au même programme de fonctionner sur des machines différentes.
- Déboguer
- Chercher et corriger les erreurs (les « bugs ») d’un programme.
Quiz final
Réponds d’abord sans regarder, puis vérifie avec les réponses plus bas.
- Pourquoi un processeur a-t-il besoin d’un compilateur pour exécuter un programme écrit en C ?
- Quel outil insère le contenu des fichiers demandés par
#includeet retire les commentaires ? - Dans l’instruction
total = x + y, combien de jetons l’analyse lexicale produit-elle, et lesquels ? - Quelle est la différence entre une erreur de syntaxe et une erreur de sens ? Donne un exemple de chacune.
- Le compilateur remplace
2 + 3par5avant l’exécution. Comment s’appelle ce genre d’amélioration ? - À quoi sert l’éditeur de liens, et pourquoi est-il nécessaire pour utiliser
printf? - Cite une différence entre un compilateur et un interprète.
- Pourquoi le même programme Java peut-il fonctionner sur des ordinateurs différents ?
Réponses
- Parce que le processeur ne comprend que le langage machine (du binaire), alors que le C est écrit avec des mots et des formules : il faut traduire de l’un vers l’autre.
- Le préprocesseur.
- Cinq jetons :
total(identificateur),=(opérateur),x(identificateur),+(opérateur),y(identificateur). - Une erreur de syntaxe enfreint la grammaire : par exemple
a + ;, où il manque une valeur. Une erreur de sens concerne une instruction grammaticalement correcte mais absurde : par exemple additionner un nombre et un texte,a + "trois". - Le calcul des constantes à l’avance (une forme d’optimisation).
- L’éditeur de liens rassemble le fichier objet avec les bibliothèques. On en a besoin pour
printfparce que son code n’est pas dans notre fichier : il est fourni tout prêt dans une bibliothèque, que l’éditeur de liens vient y chercher. - Par exemple : le compilateur traduit tout d’avance et produit un exécutable, tandis que l’interprète exécute le programme ligne par ligne sans produire de fichier exécutable.
- Parce qu’il est d’abord compilé en bytecode, puis exécuté par la machine virtuelle (la JVM) présente sur chaque ordinateur : c’est elle qui s’adapte à la machine.
Exercices de réflexion
- Imagine que tu inventes ta propre mini-langue de programmation. Écris en français, avec tes mots, une « règle de grammaire » pour une instruction qui affiche un texte (par exemple le mot
affichersuivi d’un texte entre guillemets). - Voici une instruction :
prix = 10 + ;. À quelle étape de la compilation sera-t-elle refusée, et pourquoi ? - La variable
ageest déclarée comme un entier, puis vient l’instructionage = "douze";. Cette instruction respecte-t-elle la grammaire ? A-t-elle un sens ? À quelle étape le problème est-il repéré ? - Un ami affirme : « Un programme qui compile sans erreur ne peut pas se tromper. » Es-tu d’accord ? Repense à l’histoire d’Ariane 5.
Pistes de réflexion
- Une règle possible, écrite simplement : « une instruction d’affichage est le mot
afficher, suivi d’un guillemet, d’un texte, d’un guillemet, puis d’un point-virgule. » Toute la difficulté d’une grammaire est d’éviter la moindre ambiguïté. - À l’analyse syntaxique. L’addition attend une valeur à droite du
+, et il n’y en a pas : la grammaire est enfreinte, c’est une erreur de syntaxe. - La grammaire est respectée (un nom,
=, une valeur, un point-virgule). Mais ranger un texte dans une variable déclarée entière n’a pas de sens : le problème est repéré à l’analyse sémantique, sous la forme d’une erreur de type. - Non, cette affirmation ne tient pas. Compiler sans erreur signifie que la grammaire et le sens immédiat sont corrects, mais le programme peut tout de même contenir une erreur de raisonnement qui n’apparaît qu’à l’exécution. Le code d’Ariane 5 compilait parfaitement ; c’est en vol, avec une vitesse plus grande que prévu, que le nombre est devenu trop grand pour son type.
Cours illustré sur le fonctionnement d’un compilateur · niveau 6e avancé. Toutes les valeurs, dates et noms propres ont été vérifiés avant rédaction. Le cas présenté est le cas classique d’un programme en langage C.