Compilation TP N 1 2016/2017 3 A. Licence Environnement de développement L'objectif des travaux pratiques est d'implémenter les diérentes méthodes et techniques de compilation. Il convient alors de commencer par une présentation des outils de développement disponibles 1 dans l'environnement choisi, à savoir la programmation en langage C avec gcc 2 (le compilateur C de Gnu) sous Linux. Ce premier TP ore à l'étudiant, entre autres, l'opportunité de se familiariser avec les outils communément appelés les cousins du compilateur. 1 Éditeur de texte Sur toutes les distributions Linux, on trouve plusieurs éditeurs de texte modernes en mode graphique, tels que gedit, nedit, kwrite, kate, pluma, tea, leafpad,... dont l'utilisation est similaire à celle de ceux que l'on trouve dans tous les autres environnements de développement. En convenant d'utiliser gedit 3, celui-ci peut être lancé de plusieurs manières dont on cite les deux suivantes : 1. Sélectionner la rubrique correspondante du sous-menu Accessoires du menu Applications. 2. Ouvrir une fenêtre de commandes (un terminal) en sélectionnant la rubrique Terminal MATE du sous-menu Outils système du menu Applications ; puis, lancer l'une des commandes suivantes : $ gedit & ou bien $ gedit <nom_du_fichier_à_éditer> & Le marqueur '$' désigne l'invite (ou le prompt) et ne doit pas être saisi. Le caractère '&' suggéré pour être placé à la n de, indique au shell (l'interpréteur de commandes) d'exécuter cette dernière en arrière plan (ou en tâche de fond). Ainsi, on pourra lancer l'exécution d'autres commandes (par exemple, de compilation) sans attendre la terminaison de la précédente, donc sans fermer l'éditeur de texte. Notons toutefois, qu'un utilisateur habitué d'un autre éditeur tel que vi, nedit, xemacs, nano, pico,... ou d'un environnement de développement intégré (IDE) tel que Code : :Blocks, eclipse, geany,... pourra bien-sûr l'utiliser sans aucun inconvénient. 2 Commandes de compilation Considérons un programme (squelettique) en langage C contenu dans un chier de nom prog.c. La compilation de ce programme se fait par de syntaxe générale : $ gcc [ options ] prog. c Si le programme contient la dénition de la fonction principale main() et ne contient pas d'erreurs, 1. d'autres outils seront présentés en détail dans la suite du programme. 2. Gnu C Compiler. 3. gedit est installé par défaut sur tout système Debian GNU/Linux. 1
$ gcc prog.c produira le programme exécutable qui porte par défaut le nom a.out, et qui peut être exécuté par $./a.out Principales options de gcc : -c Cette option indique que l'on ne souhaite pas eectuer l'édition des liens. Ainsi, la commande $ gcc -c prog.c produira un chier objet de nom prog.o Notons que cette option est nécessaire pour un programme ne contenant pas de fonction main(). -o <fichier_de_sortie> Cette option permet d'attribuer un nom au chier produit par la commande. Cela annule de ce fait l'appellation par défaut (a.out). Ainsi, $ gcc -o prog prog.c produira le chier exécutable de nom prog. -E Cette option indique que l'on ne souhaite pas eectuer la compilation proprement dite, mais juste préparer le programme squelettique à la compilation ; i.e., lui faire subir les pré-traitements. Ainsi, $ gcc -E prog.c permet juste de lancer le préprocesseur qui produira le programme source en langage C qui sera aché à l'écran. Il est possible de sauvegarder dans un chier la sortie de la commande précédente en lui ajoutant l'option -o prog.i ; soit $ gcc -E -o prog.i prog.c -S Cette option indique que le programme cible doit être en code assembleur. Il n'y aura ni assemblage, ni édition des liens. Ainsi, $ gcc -S prog.c produira un chier de nom prog.s contenant le résultat de la compilation du programme C en langage d'assemblage. Notons que le code assembleur produit par gcc suit la syntaxe 4 dite d'at&t, très diérente de la syntaxe dite d'intel, suivie par les assembleurs tels que MASM et NASM pour ne citer que celas. 3 Les cousins du compilateur La description ci-dessus montre bien qu'en réalité gcc ne permet par seulement d'invoquer le compilateur de même nom, mais de lancer un ou plusieurs des programmes suivants 5 : 4. AT&T (American Telephone and Telegraph) est le nom nom de la société ayant crée le système UNIX. 5. Le préprocesseur, l'assembleur et l'éditeur de liens sont parfois appelés les cousins du compilateur. 2
Le préprocesseur cpp (C preprocessor), le compilateur proprement dit, gcc (gnu C compiler), l'assembleur as (assembler) et l'éditeur de liens ld (link editor). Ces outils peuvent être invoqués par leurs propres noms. Ainsi, $ cpp prog.c permet d'invoquer le préprocesseur ; elle permet d'eectuer le même traitement que : gcc -E prog.c De même, $ cpp prog.c -o prog.i permet d'eectuer le même traitement que gcc -E prog.c -o prog.i $ as prog.s -o prog.o permet d'assembler le programme (assembleur) contenu dans le chier prog.s Cette commande produit un chier objet de nom prog.o ld prog.o permet d'invoquer l'éditeur de liens ; elle produira le programme exécutable appelé par défaut a.out. Pour changer cette appellation, on utilise l'option -o comme pour gcc. 4 Exemple de développement On se propose dans le cadre de ce premier TP, d'utiliser, d'analyser et de ce familiariser avec les outils ci-dessus. Cela permettra de prendre connaissance des diérentes étapes que traverse un programme source squelettique pour être transformé en programme exécutable. Un petit exemple de programme est proposé et utilisé dans la suite, mais l'étudiant peut bien-sûr utiliser un autre programme de sa propre création. Soit le petit programme constitué des trois chiers prog.h, prog.c et main.c dont les contenus sont : /* fichier prog. h */ # define MAX (X, Y ) (X < Y )? Y : X // Définition d ' une macro - instruction // de nom MAX et ayant deux paramètres. int max ( int, int, int, int ); // Prototype d ' une fonction de nom max. /* fichier prog. c */ # include " prog. h " # ifdef USEMACRO // USEMACRO est une variable de // compilation. int max ( int x, int y, int z, int t ){ return ( MAX ( MAX (x, y ), MAX (z, t ))); } 3
# else int max ( int x, int y, int z, int t ){ int m = x ; if (m < y ) m = y ; // Les deux fonctions retournent if (m < z ) m = z ; // le maximum des quatre nombres if (m < t ) m = t ; // fournis en arguments. return m ; } # endif /* fichier main. c */ # include " prog. h " // Pour éviter d ' inclure le header stdio. h, int main (){ // le maximum ne sera pas affiché mais return max (2,5,7,3); // retourné comme résultat à l ' interpréteur } // de commandes. La variable de compilation USEMACRO (à ne pas confondre avec une variable de programme) permettra de sélectionner laquelle des deux implémentations (dénitions) de la fonction max sera retenue pour le programme source. Notons que la première version de la fonction max() utilise la macro dénie dans prog.h alors que le seconde ne l'utilise pas. Travail à eectuer 1. Saisir ou mieux, télécharger les trois chiers prog.h, prog.c et main.c à partir du site https://compil.shost.ca 2. Lancer cpp -o prog.i prog.c puis acher prog.i et noter les prétraitements réalisés. 3. Lancer cpp -DUSEMACRO -o prog.i prog.c et comparer le résultat avec le précédent. Cette nouvelle commande dénit la variable de compilation USEMACRO. 4. Lancer gcc -S prog.i qui doit produire le programme assembleur prog.s ; ouvrir ce chier (mais sans le modier). Cette commande lance la compilation proprement dite et elle seule. 5. Assembler le chier prog.s par as -o prog.o prog.s 6. Compiler le chier main.c comme pour prog.c ou plus rapidement avec gcc -c main.c Cela doit produire le chier objet main.o 7. Lancer l'édition des liens par ld main.o et noter le message d'erreur. 8. Lancer maintenant ld main.o prog.o et... encore une erreur! Il manque une fonction de nom _start, pourquoi? L'exécution de tout programme doit commencer par une fonction _start et c'est elle qui fait appel à la fonction main(). De ce fait, gcc n'invoque pas l'éditeur des liens comme on vient de le faire, mais ajoute à la ligne de commande un certain nombre de librairies et de chiers objets qui fournissent la fonction _start. 9. Pour pouvoir générer l'exécutable de notre programme, voici la programmation en assembleur de la fonction _start, placée dans un chier debut.s (à télécharger également) : 4
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # fichier debut. s. global _start _start : push % rbp # Sauvegarde du contexte courant. mov % rsp,% rbp call main # Appel à la fonction main (). leave # Restitution du contexte. mov % rax,% rbx # Copie la valeur de retour dans # le registre % rbx. mov $1,% rax # appel système exit, nécessaire int $0x80 # pour arréter l ' exécution du # programme. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 10. Lancer maintenant as -o debut.o debut.s puis ld -o max main.o prog.o debut.o Il n'y a plus d'erreur ; on peut exécuter le programme max../max Aucune erreur d'exécution mais où est le résultat? Il constitue la valeur retournée au shell et celle-ci est achable par echo $? Et oui, le maximum des quatre nombre 2, 5, 7 et 3 (voir main.c) est 7. Remarque : Si vous développez sur un système à 32 bits, il faudra utiliser le programme debut32.s (également oert en téléchargement) à la place de debut.s 5 Autres commandes utiles cat less more <fichier_texte> Ces commandes servent à acher le contenu d'un chier texte. indent <programme_source> Cette commande sert à indenter un code source pour améliorer sa lisibilité. file <fichier> Ache le type d'un chier : archive, exécutable, texte, objet,... 5