Langage C : La ToolChain
Développer en C, la chaîne de compilation
La Toolchain
- L’éditeur de texte : permet de créer et de modifier les fichiers sources
- Le compilateur : qui permet de passer d’un fichier source à un fichier objet. Cette transformation se fait en réalité en plusieurs étapes grâce à différents composants (préprocesseur C, compilateur, assembleur).
Remarques :
- Compiler n’est pas « construire un fichier exécutable »
- Produire un fichier exécutable = compilation + édition des liens (link) + ….
Le fichier source : est un fichier texte contenant le code C du programme
Le fichier objet : est un fichier binaire contenant les instructions (code machine) et données provenant du processus de traduction du langage
- Contient le code exécutable par un processeur mais fichier non exécutable
- Organisé en sections (.text, .data, .bss, .debug, …)
- Différents formats : ELF, COFF, PE, …
Pour obtenir un fichier exécutable, il manque : l’édition des liens et la localisation
- Les liens vers les fonctions et variables externes (symboles)
- Le positionnement des fonctions et variables dans les mémoires
- Les étapes de lancement et de fin d’exécution
- L’éditeur de liens (Linker), qui assure le regroupement des fichiers objet provenant des différents modules et les associe avec les bibliothèques utilisées pour l’application. Nous obtenons ici un fichier exécutable.
-
Locator : localisation des fonctions et variables
-
Libraries : bibliothèques
- pour interfacer l’environnement (stdlib, newlib): printf, malloc, …
- Pour les fonctions usuelles (math.h, …)
- Le débogueur, qui peut alors permettre l’exécution pas à pas du code, l’examen des variables internes, etc. Pour cela, il a besoin du fichier exécutable et du code source.
Notons également l’emploi éventuel d’utilitaires annexes travaillant à partir du code source, le vérificateur de code, les enjoliveurs (beautifier), les outils de documentation automatique, etc.
ToolChain manuelle vs IDE
On remarque que la chaîne de compilation fait intervenir de nombreux outils et peut rapidement devenir complexe si le programme est constitué de nombreux fichiers source et de bibliothèques :
Deux écoles de programmeurs coexistent :
- ceux qui préfèrent disposer d’un environnement intégrant tous les outils de développement, on parle d’EDI pour Environnement de Développement Intégré ou IDE en anglais.
- ceux qui utilisent les différents utilitaires de manière séparée, configurant manuellement un fichier Makefile pour recompiler leur application.
Les IDE permettent de configurer le compilateur de générer automatiquement un Makefile, de réaliser la mise en format du code et générer documentation, etc. Ils sont très efficace, mais pour le débutant, on occulte les différentes étapes qui permettent de générer le programme. Comprendre à minima ces étapes vont nous aider pour la cross-compilation ou la résolution de bugs générés par les automatismes de l’IDE.
Les élements de la chaîne de compilation
L’éditeur de texte :
Quelques éditeurs :
- VSCode : un bon choix multiplateforme. VSCode est cependant un hybride car l’ajout de plugins le transforme en IDE pour tous les langages.
- Notepad++ : éditeur très rapide pour Windows
- Gedit ou Kate : les éditeurs pour Linux en version Gnome ou KDE
- (Neo)Vim / Emacs : les historiques, puissants et complexes à maîtriser, ont perdu grandement de leur intérêt depuis quelques années.
- Nano : quand on est obligé d’éditer du code en mode Console, plus simple que Vim.
Le compilateur :
- GCC : pour GNU Compiler Collerction qui contient gcc (en minuscule) qui est le gnu c compiler pour le C, g++ pour le c++, gccgo pour GO, … GCC fonctionne sur Linux et BSD
- MinGW[-w64] : est un portage de GCC pour un environnement Windows
- Microsoft Visual C++ : compilateur C/C++ de Microsoft
- LLVM et CLANG: interface de compilation C, C++, Objectif C, alternative à GCC.
Les autres outils :
- Make : un utilitaire qui permet d’automatiser la compilation. Pour fonctionner, il a besoin d’un fichier Makefile qui contiendra la cible, les dépendances et les commandes à exécuter.
- CMake : un moteur de production (build automaton) de plus haut niveau que Make ou SCons. Il est à placer au même niveau qu’Autotools puisqu’il permet de générer des Makefiles.
- GDB (GNU Debugger) : le débogueur standard du projet GNU.
- Doxygen : pour générer de la documentation (format HTML ou LaTeX par exemple) pour plusieurs langages de programmation (C, C++, Java, VHDL…).
- Git : logiciel de gestion de versions décentralisé, le standard du marché, inventé par Linus Torvalds également.
Les IDE
Les IDE possèdent l’avantage d’intégrer l’ensembles de outils nécessaires au développement (éditeur, compilateur, debugger, …) souvent optimisé pour un langage donné. Le graphique ci-dessous représente les parts d’utilisation des différents IDE et éditeurs de textes configurables selon un sondage de 2024 du site StackOverflow.
On remarque que VSCode est en tête, les plugins disponibles permettent de transformer l’éditeur de texte en IDE et de l’adapter à de nombreux langages de programmation (pareil pour Notepad++, Vim, Sublim Text).
Ce sondage est cependant à prendre avec beaucoup de hauteur car si l’on regarde en fonction des langages de programmation:
- PyCharm est très utilisé en Python
- Visual Studio est le standard quand on veut développer dans les technos Microsoft,
- Eclipse ou IntelliJ pour du Java (Android Studio est basé sur IntelliJ)
Les fabriquants de micro-contrôleurs se basent sur ces IDE pour développer leurs outils de programmation, par exemple:
- STM32CubeIDE est basé sur Eclipse pour programmer les STM32 en C
- l’Arduino IDE 2.0 est basé sur Eclipse également
- MPLAB X est basé sur NetBeans (IDE pour Java) pour programmer les PIC en C
- VSCode + le Plugin PlateformIO est utilisé pour programmer les ESP32
IDE pour le langage C
A l’IUT, le choix s’est porté sur l’IDE Code::Blocks avec le compilateur MinGW pour Windows :
- logiciel libre -> gratuit
- multiplateforme
- léger et facile à installer
- simplicité d’utilisation qui le rend particulièrement adapté pour l’apprentissage.
Pour la programmation en C sur Linux, vous pouvez utiliser VirtualBox sur Windows et y installer Ubuntu. Dans Ubuntu, il restera à installer le paquet build-essential pour avoir accès aux compilateurs et VSCode si vous souhaitez travailler avec cet éditeur.
Premier programme en C et Compilation sous Linux
En hommage au Livre “The C programming Langage de K&R”, il est de tradition de commencer l’apprentissage de la programmation en affichant Hello world! sur l’écran.
/*
Date : 2022 08 09
Auteur : Philippe Celka
Nom : main.c
Résumé : Affiche Hello world! à l'écran
*/
#include <stdio.h> //Bibliothèque pour la fonction printf
int main(void)
{
printf("Hello world! \n");
return 0;
}
Compilation et Édition de liens
Dans cet exemple, le code source hello.c est enregistré dans le répertoire /home/philippe/Documents/Test_c/
Nous pouvons nous placer dans ce répertoire et ouvrir un terminal Linux. Nous utiliserons gcc pour compiler notre programme et générer le fichier exécutable.
gcc s’utilisera de la manière suivante :
gcc [fichier c à compiler] -o [exécutable]
-o correspond à l’option outfile et permet de spécifier le nom pour le fichier de sortie.
gcc main.c -o hello
On remarque que dans le répertoire, un nouveau fichier apparaît, un fichier exécutable.
Pour exécuter ce fichier, depuis le terminal, nous utiliserons la commande
./hello
Hello world! s’affiche dans la console, vous avez réalisé votre premier programme.
Remarque : gcc dans ce cas de figure, n’effectue pas seulement la compilation mais également l’édition des liens pour générer l’exécutable.
Compilation seule et fichier objet
Pour utiliser uniquement le compilateur de gcc et générer le fichier objet, on utilise l’option -c
gcc main.c -c -o main.o
Pour afficher le contenu du fichier objet, nous utiliserons objdump
objdump -S main.o
Nous obtenons le code machine et le code assembleur suivant :
main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <main+0xf>
f: 48 89 c7 mov %rax,%rdi
12: e8 00 00 00 00 call 17 <main+0x17>
17: b8 00 00 00 00 mov $0x0,%eax
1c: 5d pop %rbp
1d: c3 ret
Il s’agit d’une section du code. Le linker réalisera ensuite le lien entre les différents fichiers objet et bibliothèques pour générer l’exécutable. (Je ne développerai pas ces opérations dans cet article)
Compilation et édition de liens avec plusieurs fichiers
Nous souhaitons maintenant compiler deux fichiers c pour obtenir un exécutable :
/*
Nom : main.c
*/
#include <stdio.h>
int affichage1(void); // prototype de la fonction affichage1
int main(void)
{
printf("Hello world! \n");
affichage1(); // appel de la fonction affichage1
return 0;
}
/*
Nom : affichage.c
*/
#include <stdio.h> //Bibliothèque pour la fonction printf
int affichage1(void)
{
printf("Ceci est un message affichage1 \n");
return 0;
}
Pour générer l’exécutable, gcc va compiler main.c pour en faire un main.o (objet), affichage.c devient un affichage.o et le linker va lier les deux fichiers pour générer l’exécutable test.
gcc main.c affichage.c -o test
Fichier .h
//Nom : main.c
#include <stdio.h>
#include "affichage.h" //fichier header
int main(void)
{
printf("Hello world! \n");
affichage1();
affichage2();
affichage3();
return 0;
}
//Nom : affichage.c
#include <stdio.h> //Bibliothèque pour la fonction printf
int affichage1(void)
{
printf("ALEA \n");
return 0;
}
int affichage2(void)
{
printf("JACTA \n");
return 0;
}
int affichage3(void)
{
printf("EST \n");
return 0;
}
//Nom : affichage.h
#ifndef _AFFICHAGE_H_
#define _AFFICHAGE_H_
int affichage1(void);
int affichage2(void);
int affichage3(void);
#endif
gcc main.c affichage.c -o test
ALEA, JACTA, EST s’affiche correctement, notre chaîne de compilation fonctionne avec plusieurs fichiers.
Pour le moment, nous avons deux fichiers c à compiler. Un programme complet va en contenir plusieurs dizaines ou beaucoup plus. Le moteur de jeu comme Unity dépasse le Million de lignes de code, des centaines de fichiers, il est alors nécessaire de structurer le projet et d’automatiser le processus de compilation.
Présentation de Makefile
Présentation
Un Makefile est un fichier constitué de plusieurs règles de la forme :
Sélectionnez
cible: dependance
commandes
Chaque commande est précédée d’une tabulation. Lors de l’utilisation d’un tel fichier via la commande make la première règle rencontrée, ou la règle dont le nom est spécifié, est évaluée.
L’évaluation d’une règle se fait en plusieurs étapes :
- Les dépendances sont analysées, si une dépendance est la cible d’une autre règle du Makefile, cette règle est à son tour évaluée.
- Lorsque l’ensemble des dépendances est analysé et si la cible ne correspond pas à un fichier existant ou si un fichier dépendance est plus récent que la règle, les différentes commandes sont exécutées.
Makefile minimal
Pour notre exemple, le makefile minimal est :
test: affichage.o main.o
gcc -o test affichage.o main.o
affichage.o: affichage.c
gcc -o affichage.o -c affichage.c
main.o: main.c affichage.h
gcc -o main.o -c main.c -W
Regardons comment fonctionne ce Makefile :
Nous cherchons à créer le fichier exécutable test, la première dépendance est la cible d’une des règles de notre Makefile, nous évaluons donc cette règle. Comme aucune dépendance de affichage.o n’est une règle, aucune autre règle n’est à évaluer pour compléter celle-ci.
Deux cas se présentent ici : soit le fichier affichage.c est plus récent que le fichier affichage.o, la commande est alors exécutée et affichage.o est construit, soit affichage.o est plus récent que affichage.c est la commande n’est pas exécutée. L’évaluation de la règle affichage.o est terminée.
Les autres dépendances de test sont examinées de la même manière puis, si nécessaire, la commande de la règle test est exécutée et test est construit.
On crée le makefile avec un éditeur de texte, en faisant attention d’utiliser des tabulations et non des espaces pour les règles. On lance la commande make qui va exécuter les règles de compilation issues du makefile.
Makefile “enrichi”
Plusieurs cas ne sont pas gérés dans l’exemple précédent :
- Un tel Makefile ne permet pas de générer plusieurs exécutables distincts.
- Les fichiers intermédiaires restent sur le disque dur même lors de la mise en production.
- Il n’est pas possible de forcer la régénération intégrale du projet
Ces différents cas conduisent à l’écriture de règles complémentaires :
- all : généralement la première du fichier, elle regroupe dans ces dépendances l’ensemble des exécutables à produire.
- clean : elle permet de supprimer tous les fichiers intermédiaires.
- mrproper : elle supprime tout ce qui peut être régénéré et permet une reconstruction complète du projet.
En ajoutant ces règles complémentaires, notre Makefile devient donc :
all: test
test: affichage.o main.o
gcc -o test affichage.o main.o
affichage.o: affichage.c
gcc -o affichage.o -c affichage.c
main.o: main.c affichage.h
gcc -o main.o -c main.c -W
clean:
rm -rf *.o
mrproper: clean
rm -rf test
Après avoir généré l’exécutable, on peut lancer la commande make clean pour supprimer les fichiers objets.
La réalisation d’un makefile mérite un article spécifique. On n’a même pas affleuré les possibilités de cet outils. L’objectif de cette présentation de makefile était de présenter les différentes étapes que gère un IDE quand vous appuyez sur le bouton play.
Conclusion
Compiler un programme avec les outils gcc et make peut rapidement devenir complexe avec l’augmentation du nombre de fichiers.
L’utilisation d’un IDE comme Code::Blocks va générer de manière transparente le bon makefile pour compiler votre projet. Par contre, comme dans tout automastisme, il y a des cas de figures où cela ne fonctionne pas. Maîtriser à minima les différentes étapes de compilation permet de mieux comprendre les erreurs remontées par l’IDE et trouver une solution.
Lorsque l’on appuye sur le bouton play de l’IDE, de nombreuses étapes sont effectuées en toute transparence pour l’utilisateur pour générer l’exécutable. Le programmeur peut se concentrer sur la qualité de son code.
Pour le développement des systèmes embarqués, maîtriser un peu plus en profondeur les étapes de compilation est nécessaire.