Savez-vous ce qui se passe exactement lorsque vous utilisez un compilateur C comme gcc ?
Disclaimer
Pendant ma formation, je devais écrire des articles techniques. Ici, c’est l’article que j’ai écris à propos de gcc
. Il s’agit d’une traduction de la version rédigée en anglais, langue requise dans le cadre de la formation.
Bonjour à tous.
J’ai récemment commencé une formation pour changer de carrière et devenir développeur. Cette formation consiste, bien sûr, à apprendre à coder, mais aussi à communiquer avec d’autres personnes et, en particulier, à rédiger de la documentation technique. Dans cette optique, je rédige un article de vulgarisation. Cet article doit être conçu de manière à être compréhensible même par un débutant.
C’est pourquoi, bien que nous aborderons des cas spécifiques et des concepts techniques assez avancés, je ne vais pas entrer dans tous les détails et je ferai quelques raccourcis. L’objectif est d’aller à l’essentiel et de se concentrer sur ce qui est vraiment important à comprendre.
Définitions
Socrate aurait dit :
“Le début de la sagesse est la définition des termes.”
Commençons donc par quelques définitions afin de bien comprendre dans quoi nous nous engageons :
GCC
GGC signifie GNU Compiler Collection. Il s’agit d’un ensemble de compilateurs permettant de compiler divers langages de programmation tels que C, C++ et Java, pour n’en citer que quelques-uns (dans cet article, nous nous concentrerons principalement sur le langage C). Cette définition, bien que utile, nous oblige à examiner deux autres définitions. La première est celle du compilateur et la seconde, celle du langage C.
Langage C
Le langage C a été créé en 1972 par Dennis Ritchie et Brian Kernighan. Ce langage a permis de rédiger le système d’exploitation UNIX (rien que ça !).
En fait, la fin des années 60 marque le début de l’informatique, et les systèmes d’exploitation commencent à être écrits. Pour rédiger ces systèmes, on écrit des instructions envoyées aux processeurs des ordinateurs. Ces instructions sont rédigées dans un langage que les puces peuvent comprendre : l’assembleur (souvenez-vous-en, nous en reparlerons plus tard).
Cependant, ce langage n’est pas très pratique à utiliser, d’abord parce que sa syntaxe est assez abstraite et éloignée du langage humain ; et ensuite, parce que chaque processeur parle son propre langage, il faut donc adapter ce code assembleur dès qu’on veut le faire fonctionner sur une autre machine.
Voici un exemple entre C et l’assembleur :
C’est pourquoi des langages comme C ont été créés. Non seulement pour avoir une syntaxe qui ressemble davantage au langage humain, mais aussi pour pouvoir porter le code que nous écrivons. En fait, on peut convertir le même code écrit en C pour une machine A qui n’utilise pas le même processeur qu’une machine B. C’est ce qu’on appelle un compilateur, et la transition est parfaite !
Compilateur C
Un compilateur est un programme qui traduit le code source (dans ce cas, le code C) en un langage exécutable par la machine.
Comment fonctionne GCC
Fonctionnement de base
Comme nous l’avons vu plus tôt, nous comprenons désormais comment fonctionne un compilateur, et nous allons nous intéresser de plus près à un compilateur particulier : GCC (ou gcc dans le reste de l’article, car les commandes sont souvent écrites en minuscules).
Vous penseriez qu’il suffit de donner un fichier écrit en C en entrée (ce sont des fichiers avec l’extension .c) et qu’il en ressortira un code exécutable.
Mais en réalité, beaucoup de choses se passent en coulisses… Laissez-moi vous montrer ce qui se passe sous le capot !
Sous le capot…
Voici tout ce qui se passe sous le capot de gcc, et tout ce qu’il fait en arrière-plan dont nous ne savons même pas qu’il se passe ! Voyons cela étape par étape…
Étape 0 – a : Fichiers sources
Imaginons que nous voulons compiler un fichier que j’appelle main.c. Voici le code :
#include <stdio.h>
int main(void)
{
printf("Hello, World !\n");
return (0);
}
C’est ce qu’on appelle le code source, c’est-à-dire toutes les instructions nécessaires à l’exécution de notre programme pour faire ce que l’on veut. Ici, je veux afficher les mots “Hello World !” à l’écran (pourquoi est-ce toujours Hello World ?). Pour compiler mon fichier, je dois taper la commande suivante :
gcc main.c
(Bien sûr, vous devrez adapter “main.c” au nom de votre fichier… Cela s’applique à chaque fois que vous verrez “main.c” dans le reste de cet article.)
Mais que se passe-t-il ensuite ?
Étape 0 – b : Fichiers d’en-tête
Avant de regarder ce qui se passe avec notre main.c,, nous devons aussi savoir qu’il existe des fichiers appelés en-têtes.
Ces fichiers indiquent toutes les fonctions que nous voulons rendre disponibles pour d’autres fichiers. C’est comme un index pour savoir rapidement les ingrédients dont nous aurons besoin dans notre code, sans avoir à passer par tout le code pour savoir ce dont nous avons besoin. Ils permettent aussi de structurer et de partager des définitions entre plusieurs fichiers sources et facilitent la gestion de projets à grande échelle en évitant de répéter plusieurs fois les mêmes choses dans plusieurs fichiers. Pour simplifier, je ne l’ai pas inclus dans cet exemple, mais ce type de fichier existe bien.
Étape 1 – Préprocesseur
Le prétraitement est la toute première étape réalisée par le compilateur. C’est une sorte de préparation pour le reste des tâches. Typiquement, dans l’exemple précédent, nous voyons que nous utilisons #include. En d’autres termes, nous voulons utiliser une bibliothèque de fonctions existante. Le préprocesseur va alors récupérer le contenu de la bibliothèque et l’inclure dans notre code, prêt à être compilé. Le préprocesseur supprimera également les commentaires afin de ne pas interférer avec le reste du processus et prendra en charge d’autres tâches (comme la gestion de #define), mais ce n’est pas le sujet de cet article et cela mériterait un article à part.
Si vous ne demandez rien de particulier à gcc, ces tâches sont réalisées de manière invisible. Cependant, si vous voulez voir ce qui se passe à la fin de cette étape de prétraitement, tapez la commande suivante pour générer ces fichiers prétraités au format `.i` et arrêter la compilation après cette tâche :
gcc -E main.c
Étape 2 – Compilateur
Vient ensuite le compilateur. Le compilateur prend le code source (après qu’il ait été nettoyé par le préprocesseur) et le traduit en langage assembleur, qui est une représentation textuelle de ce que sera le futur code machine. Il vérifie également qu’il n’y a pas d’erreurs dans le code (erreurs de syntaxe, ou fonctions mal utilisées). Si des erreurs sont détectées, gcc nous les signale pour que nous puissions les corriger. Plutôt sympa, non ?
Encore une fois, la génération et l’utilisation de ces fichiers `.s` sont cachées, et si vous voulez les afficher, tapez :
gcc -S main.c
Étape 3 – Assembleur
Nous arrivons maintenant à l’étape de l’assembleur. Cette étape prend les fichiers d’assembleur vus à l’étape 2 et les convertit en code machine (c’est-à-dire en code binaire) dans un format que le processeur peut exécuter. Les fichiers ainsi créés sont des fichiers objets se terminant par .o.
Comme prévu, ces fichiers sont générés et utilisés de manière invisible, et si vous souhaitez les afficher, utilisez la commande :
gcc -c main.c
Si vous avez suivi, nous avons mentionné le langage assembleur au début de cet article. Eh bien, avant l’invention du C, les développeurs devaient coder directement en assembleur, et c’est exactement ce type de fichier que l’étape de l’assembleur crée dans GCC. Rien n’est laissé au hasard !
Étape 4 – Éditeur de liens
Nous arrivons maintenant à l’étape finale. Comme indiqué dans notre diagramme, l’assembleur crée plusieurs fichiers objets .o. Bien que ces fichiers soient en langage machine, et que l’assembleur soit capable de les lire indépendamment les uns des autres, il ne peut pas les lier entre eux. C’est là qu’intervient l’éditeur de liens : il réunit ces différents fichiers .o en un seul fichier exécutable complet. Il peut être utilisé pour résoudre les références (si, par exemple, des fichiers objets font référence à des fonctions définies dans d’autres fichiers, comme printf dans notre exemple). Il fait le lien entre les bibliothèques, connectant les bonnes fonctions et variables aux bons endroits dans le programme.
Étape 5 – Notre fichier exécutable !
Si tout s’est bien passé et que GCC ne nous a pas signalé d’erreurs, notre commande a généré un fichier `a.out` (le nom du fichier généré par défaut) et nous pouvons l’exécuter pour afficher fièrement notre “Hello World !”.
gcc main.c
Et maintenant ?
Bien que ce soit un langage ancien, le C est toujours largement utilisé aujourd’hui. Pourquoi ? D’abord parce qu’il est le “père” de tous les langages modernes. Des langages comme Python, C++, Java, Javascript et Perl ont tous été influencés par ce langage, donc connaître le C permet d’acquérir une certaine logique et de rapidement prendre en main ces langages plus modernes. Ensuite, en utilisant le C, il y a très peu d’étapes et d’intermédiaires entre les instructions du code et leur exécution par le processeur. Cela permet de mieux maîtriser l’utilisation de la mémoire, du processeur et des temps d’exécution, ce qui est très utile pour coder des applications nécessitant des performances élevées et des logiciels informatiques de base.
C’est tout, les amis !
Merci d’avoir lu cet article jusqu’au bout, et n’hésitez pas à commenter si vous avez des questions ou des remarques.
À bientôt pour de nouvelles aventures !
Comments by OursBlanc