Quelques éléments de compilation en C et makefiles
Transcription
Quelques éléments de compilation en C et makefiles
Quelques éléments de compilation en C et makefiles Guillaume Feuillade 1 Compiler un programme C Le principe de la compilation consiste à passer d’un ensemble de fichiers de code à un programme exécutable sur une machine. Cette transformation est effectuée par un ensemble de programmes qu’il est important de bien comprendre et maîtriser. 1.1 Commandes de compilation On trouve généralement, sur les machines Unix du U3, les commandes suivantes permettant d’effectuer la compilation de programmes en C : cc, gcc... Je vous recommande personnellement d’utiliser de préférence le programme gcc, en particulier pour ses messages d’erreurs qui sont souvent plus explicites et utiles. 1.2 Compiler un programme comportant plusieurs fichiers de code vous avez probablement déjà utilisé un programme de compilation de la manière suivante : gcc fichier.c à partir d’un fichier de code fichier.c, le programme gcc construit un fichier exécutable dont le nom est celui par défaut a.out. Peut être avez-vous déjà utilisé l’option -o de la manière suivante : gcc fichier.c -o executable l’option -o permet de spécifier quel sera le nom de la sortie du programme gcc, ici c’est l’exécutable généré. Dans le cas de la compilation de plusieurs fichiers. Vous avez peut-être également utilisé une commande de cette sorte : gcc fichier1.c fichier2.c -o executable cette ligne permet de générer le fichier exécutable à partir de plusieurs fichiers de code. Cette méthode pour générer un exécutable est fortement déconseillée car elle cache les étapes intermédiaires et conduit souvent à des erreurs de la part du programmeur. 1.2.1 Compilation et édition de liens, principe Il est important de bien comprendre le processus de compilation. Le problème qui se pose quand on compile plusieurs fichiers de code est qu’il y a dans certains de ces fichiers des appels à des fonctions ou variables qui apparaissent dans d’autres fichiers. À cause de ce fait, la compilation d’un code écrit en C se décompose en plusieurs étapes. Supposons que l’ont ait des fichiers de code fichier1.c, fichier2.c, ... fichiern.c. Pour passer de ces multiples fichiers de code à un exécutable, on effectue les étapes suivantes : 1 1. pour chacun des fichier fichieri.c on effectue l’étape de compilation. Cette étape consiste à construire un fichier binaire (dit fichier objet), presque compréhensible par la machine. Cependant, comme certaines fonctions ou variables utilisées dans ce code ne sont pas définie dans le fichier (ils sont définis dans d’autres des fichiers de code), il n’est pas possible de traduire leurs appels en langage machine. Par conséquent ces appels sont laissés tels quels dans le fichier binaire produit. Le résultat est un fichier, usuellement nommé fichieri.o qui n’est pas exécutable. 2. Une fois que tous les fichiers .o ont été générés, par autant de compilations, on peut effectuer l’étape d’édition de lien. Lors de cette étape, le programme d’édition de lien va mettre bout-à-bout tous les fichiers .o nécessaires puis va résoudre les appels qui ont été laissés durant l’étape de compilation. Résoudre les appels consiste à remplacer l’appel à la fonction par un lien (un saut) vers la fonction elle-même (qui se trouve à un endroit différent dans le binaire). Enfin, l’édition de lien positionne le point de départ du programme (la première ligne de la fonction main). 1.2.2 Compilation et édition de liens, application En utilisant gcc on peut (et on va) effectuer ces deux étapes séparément. Dans un premier temps, on effectue la compilation avec l’option -c : gcc -c -Wall fichier1.c gcc -c -Wall fichier2.c ... cela nous permet d’obtenir les fichiers binaires fichier1.o, fichier2.o, ... Nous pouvons maintenant démarrer l’étape d’édition de lien. Il suffit pour cela de passer les fichier .o à gcc de la manière suivante : gcc fichier1.o fichier2.o ... -o executable On obtient enfin le fichier exécutable, qu’on pourra alors lancer directement en tapant : ./executable Remarque : prenez l’habitude d’effectuer l’étape de compilation avec l’option -Wall qui active tous les warnings. Ceux-ci sont souvent très utiles pour détecter des erreurs qui passent à la compilation mais causent des problèmes à l’exécution. 1.3 Utilité des fichiers header Lorsque l’on souhaite obtenir un exécutable à partir d’un code réparti sur plusieurs fichiers, on a vu qu’il était nécessaire de compiler chacun des fichiers .c séparément. Lorsqu’un de ces fichiers de code utilise par exemple une fonction définie dans un autre fichier, même si lors du processus de génération du .o il ne va pas traiter les appels à cette fonction mais il a tout de même besoin de connaître la signature de la fonction pour être en mesure de faire les vérifications de type nécessaires à la compilation. Pour cela on utilise des fichiers header avec le principe suivant : pour chaque fichier .c, par exemple fichier.c, qui définit des fonctions ou variables susceptibles d’être utilisés dans d’autre fichiers .c il est nécessaire d’écrire un fichier header correspondant avec l’extension .h (fichier.h dans notre exemple). Ce fichier header contient les signatures des fonctions et variables utilisables dans d’autres fichiers de code. Les signatures sont données comme sur l’exemple suivant : 2 int f(int, char *) Lorsqu’un autre fichier de code utilise une de ces fonctions, il doit inclure le fichier header dans son préambule de la manière suivante : #include”fichier.h” À noter que la syntaxe du #include est différente de celle des librairies standard (pour des raisons de chemin d’accès) ; en effet, on utilise la syntaxe suivante : #include<stdlib.h> 1.4 Headers et compilation Que se passe-t-il lorsque l’on compile un fichier contenant la directive #include”fichier.h” ? La toute première étape de la compilation est le passage du préprocesseur C. Celui ci a pour rôle, pour simplifier, de supprimer les commentaires et de traiter les instructions commençant par # (macros etc.). Pour le cas du #include”fichier.h”, le travail du préprocesseur consiste simplement à aller chercher le fichier inclus et à le copier à l’emplacement de l’instruction. Par conséquent, inclure un fichier header est équivalent à recopier au début du fichier les signatures des fonctions et variables que l’on souhaite inclure. Remarque : dans des projets un peu complexe, il n’est pas toujours évident de bien planifier les inclusions pour éviter que la même fonction ne soit définir plusieurs fois (ce qui engendre une erreur de compilation). Pour cette raison il est parfois d’usage d’encadrer le fichier header de la manière suivante (exemple pour notre fichier.c) : #IFNDEF _FICHIER.H_ #DEFINE _FICHIER.H_ ... signatures des fonctions et variables ... #ENDIF L’utilité de cette méthode est que, même si le fichier header se trouve inclus plusieurs fois, il ne sera conservé qu’en un unique exemplaire par le préprocesseur. 2 Utilisation de fichiers makefile pour la compilation L’objectif de cette section est de (re-)voire la méthode pour automatiser la compilation de programmes en C en utilisant des fichier makefile. 2.1 Principes Un fichier makefile est un fichier spécifiquement conçu pour être utilisé par un programme (make ou gmake). Le fichier doit impérativement se nommer makefile pour être correctement sélectionné par make.Le but de ces programmes est d’automatiser les processus liés à la compilation de code ainsi que certaine autres tâches. Il existe d’autres programmes et d’autre formats de fichiers qui permettent de faire 3 des choses équivalents, mais dans le cadre des TP nous allons utiliser les fichiers makefile. À noter que dès qu’un code dépasse les 2 fichiers à compiler il est impératif de chercher à automatiser les tâches de compilation, une approche manuelle des compilation serait alors une perte de temps et une source supplémentaire d’erreurs. Un fichier makefile contient des règles de la forme suivante (séparées par des lignes vides) : cible : dépendance [tabulation] commandes la [tabulation] correspond bien à l’espacement obtenu. grâce à la touche du même nom. Attention à bien respecter cette syntaxe. La commande peut si besoin être exprimée sur plusieurs lignes, chacune commençant par une tabulation. Une règle se comprend de la manière suivante : pour fabriquer la cible, il faut d’abord vérifier que les dépendances sont à jour, en les produisant dans un premier temps si nécessaire. Lorsque c’est le cas, et que les dépendances sont plus récentes que la cible, make va alors exécuter les commandes pour produire la cible. Parfois la cible n’est pas produite, c’est la cas dans la règle suivante, qui ne produit pas de fichier : clean : rm *.o 2.2 Un makefile simple Nous allons définir un fichier makefile pour le cas suivant : – on dispose d’un fichier erreur.c qui définir la fonction erreur – on a écrit le fichier header erreur.h correspondant – on a écrit un fichier exo1.c qui contient la fonction main et qui utilise la fonction erreur (et donc qui inclut le fichier erreur.h). Dans ce cas, voici un exemple de makefile que nous pouvons définir : exo1 : exo1.o erreur.o gcc exo1.o erreur.o -o exo1 exo1.o : exo1.c erreur.h gcc -c -Wall exo1.c erreur.o : erreur.c gcc -c -Wall erreur.c clean : rm *.o Voici le fonctionnement de ce fichier makefile : lorsque vous voulez construire votre exécutable exo1 vous pouvez taper make exo1 pour demander à make de traiter la règle correspondante. Vous pouvez 4 également taper uniquement make (dans ce cas make traite par défaut la première règle du fichier). Voici ce que make va faire : 1. regarder les dépendances de la règle exo1. Il faut que exo1.o et erreur.o soient à jour. 2. on commence par exo1.o. On sélectionne la règle correspondante et on regarde les dépendances : exo1.c et erreur.h. Pour ces deux dépendances on ne dispose pas de règle, on vérifie donc juste leur présence et on compare leurs dates à celle du fichier exo1.o – si exo1.o existe et est plus récent que exo1.c et erreur.h alors il est à jour et il n’y a rien à faire. – si exo1.o n’existe pas ou est plus ancien que exo1.c où erreur.h, dans ce cas il faut le refabriquer. Pour cela make exécute la commande correspondante : gcc -c -Wall exo1.c. 3. On procède ensuite de même pour erreur.o, il sera généré si il n’existe pas ou est plus ancien que erreur.c 4. Maintenant que les dépendances sont à jour, on peut s’occuper de exo1. Si celui-ci n’existe pas ou est plus ancien que exo1.o ou erreur.o, on va alors exécuter gcc exo1.o erreur.o -o exo1. Remarquons que si on travaille quelque temps sur exo1.c et qu’on ne modifie plus erreur.c alors make ne redemandera pas la compilation de erreur.c à chaque fois qu’il est invoqué, ce qui serait inutile. Remarquons également que l’on a mis erreur.h dans les dépendance de exo1.o, l’idée derrière ceci est de forcer à recompiler exo1.c si l’on décidait de modifier la signature de la fonction erreur. 2.3 Améliorer son fichier makefile Le fichier makefile présenté ci-dessus est très simple. Il est suffisant pour un projet simple, mais il existe beaucoup d’options et de simplifications disponibles pour les fichiers makefile. Je vous encourage à aller chercher des documentations pour améliorer vos fichiers makefile. N’oubliez pas que les outils de compilation, de type make sont des outils que vous serez probablement amenés à utiliser dans un cadre professionnel, il est dont entièrement profitable de s’auto-former sur ce type d’outils : le bénéfice ira beaucoup plus loin que les TPs de PPU. 2.4 Un makefile de départ Sur la page-web où se trouve ce document, vous trouverez un fichier makefile. Ce fichier est prévu pour être utilisé dès l’exercice 2. Comme la première règle (cible) a pour dépendance exo2, il suffit de taper make pour fabriquer l’exécutable. Quand vous passerez aux exercices suivant, il faut rajouter les règles correspondantes (par exemple pour exo4.o et exo4), et de modifier la ligne cible pour mettre l’exercice courant par défaut (dans notre exemple exo4. Vous pouvez taper make clean pour supprimer les fichiers .o. 5