Les étapes de la compilation
Le pré-processeur
Comme vous l’avez vu au TP précédent, un fichier contenant toutes les sources ce n’est pas très pratique. Imaginez que le code d’un OS (~8*10^6) soit regroupé dans un seul fichier. Cela devient impossible à gérer! Nous allons donc séparer le code en plusieurs fichiers, et c’est ce que vous allez faire avec votre code.
Pour commencer, répartissez le code contenu dans le fichier main.c:
- mettez les signatures des fonctions (new_link, chain, display_linked_list, free_list, sort, search, insert, read_file_content) dans un fichier linked-list.h
- mettez la définition des types de données (Link_t et Student_t) dans un fichier link-types.h
- mettez le code des fonctions (new_link, chain, display_linked_list, free_list, sort, search, insert, read_file_content) dans un fichier linked-list.c
- laisser le reste du code (définition des variables globales et fonction main) dans le fichier main.c: supprimer les sections de code que vous avez copiées dans d’autres fichiers.
Vous allez maintenant compiler un programme dont le code C a été réparti sur plusieurs fichiers « .c ». Pour la compilation de trois fichiers fic1.c, fic2.c, et fic3.c, vous devrez exécuter la commande:
$gcc -Wall fic1.c fic2.c fic3.c -o mon_prog
L’exécution de cette ligne de commande avec les fichiers main.c et linked-list.c devrait vous renvoyer des erreurs et warning du type:
error: unknown type name 'Link_t' warning: implicit definition of function 'read_file_content'
L’erreur vient du fait que le type Link_t est utilisé, mais le compilateur ne trouve pas sa définition. Utilisez la directive #include pour remédier à ce type problème.
Le warning vient du fait que la fonction read_file_content est appelée, mais le compilateur ne connait pas la signature de cette fonction.
Exercice 1: Utilisez la directive #include pour remédier à ce type problème.
Maintenant, vous pouvez avoir des erreurs de redéfinition de type. Pour être sûr de mettre en évidence le problème, incluez linked-list.h puis link-types.h dans le fichier linked-list.c.
error: typedef redefinition with different types ('struct Link_t' vs 'struct link')
Ceci est du au fait que vous incluez plusieurs fois le même fichier .h, et donc vous définissez plusieurs fois le même type de données. Tapez la commande suivante pour vous en convaincre:
gcc -E linked-list.c
Exercice 2: Utilisez les directives #ifndef /#endif et #define pour remédier à ce type problème.
Des erreurs de compilation, encore? Et bien oui: vous devriez avoir des erreurs du type:
error: use of undeclared identifier 'number_of_students'
Ceci est dû au fait que lors de la compilation dans linked-list.c, vous utilisez une variable qui n’a pas été déclarée.
Exercice 3: déclarez la variable number_of_students avant de l’utiliser dans le fichier linked-list.c pour remédier à ce type de problèmes. Attention cependant à ne pas DEFINIR cette variable deux fois! (Utilisez le mot clé extern pour vous en assurer).
Normalement, plus d’erreur ni de warning, merci gcc ! 😉
La compilation séparée
Imaginez que le code d’un OS (~8*10^6) soit regroupé dans un seul fichier. Cela devient très (très!) long à compiler! Nous allons donc compiler les fichiers « .c » séparément. et c’est ce que vous allez faire avec votre programme.
Essayez pour commencer:
gcc -Wall linked-list.c
Mille milliards de milles sabord, encore des erreurs de compilation!
Undefined symbols for architecture x86_64: "_main", referenced from: implicit entry/start for main executable ld: symbol(s) not found for architecture x86_64
La raison est simple: vous avez essayé de produire un fichier executable à partir d’un fichier « .c » qui ne contient pas d’implémentation de la fonction main. gcc ne sait pas par où commencer l’exécution du programme…
Exercice 4: utiliser l’option -c pour produire des fichiers objets:
- gcc -Wall -c main.c L’effet de cette commande: le fichier main.c est compilé, et un fichier objet, main.o, est produit.
- gcc -Wall -c linked-list.c L’effet de cette commande: le fichier linked-list.c est compilé, et un fichier objet, linked-list.o, est produit.
Exercice 5: vérifiez le format des fichiers objets en utilisant la commande file :
$ file linked-list.o foncs.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped $ file main.o main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
Exercice 6: visualisez quels sont symboles connus dans les fichiers objets en utilisant la commande nm
$ nm main.o $ nm linked-list.o
Interprétation du résultat:
T —–> le symbole est défini dans le fichier, c’est une fonction,
B —–> le symbole est défini dans le fichier, c’est une variable globale, non initialisée
C —–> le symbole est défini dans le fichier, c’est une variable globale, initialisée à une valeur différent de 0 à la définition
U —–> le symbole n’est pas défini dans ce fichier, il doit se trouver dans une bibliothèque, ou dans un autre fichier objet.
Vérifier que :
- main et number_of_students sont définis dans main.o;
- chain, display_linked_list, free_list, insert, etc. sont NON définis dans main.o
- chain, display_linked_list, free_list, insert, etc. sont définis dans linked-list.o;
- plusieurs symboles ne sont connus dans aucun des deux, par exemple : strcmp, scanf, printf.
L’édition de lien
- gcc linked-list.o main.o -o linked-list-exe L’effet de cette commande: un fichier executable, linked-list-exe, est produit. Vérifiez son bon fonctionnement.
Exercice 7: vérifiez le format du fichier exécutable en utilisant la commande file :
$file linked-list-exe appli: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=0x294d2160a3f70a1b0467e926454fdaa64a5c5016, not stripped
Exercice 8: visualisez quels sont symboles connus dans les fichiers objets en utilisant la commande nm:
$nm linked-list-exe
La sortie de nm montre que certains symboles ne sont toujours pas définis dans cet exécutable, par exemple: printf et strcmp.
Pour avoir la liste des bibliothèques dites « dynamiques » nécessaires pour exécuter appli, exécuter la commande :
$ ldd linked-list-exe
On obtient une liste du type :
linux-vdso.so.1 libc.so.6 => /lib64/libc.so.6 /lib64/ld-linux-x86-64.so.2
L’utilitaire strip retire la table des symbole, l’exécutable est plus petitt :
$ ls -l linked-list-exe -rwx------ 1 domas ir_domas 13572 6 déc. 13:57 linked-list-exe $ strip linked-list-exe $ ls -l linked-list-exe -rwx------ 1 domas ir_domas 13308 6 déc. 13:57 linked-list-exe
Par défaut, l’édition de lien est dynamique. Pour réaliser une édition de lien statique, nous devons utiliser l’option -static de gcc.
Création et utilisation de bibliothèque (library en anglais)
Certaines fonctions peuvent être utilisées, ou ré-utilisées par plusieurs program (printf par exemple). Ces fonctions sont alors regroupées dans des bibliothèques.
Une bibliothèque (statique) n’est rien d’autre qu’une archive qui regroupe plusieurs fichier objets en un seul. Pour créer une librairie, on utilisera l’outil « ar » (ar comme archive). La principale utilisation de ar est la construction de bibliothèques. On va reprendre l’exercice précédent et créer une bibliothèque pour y ranger les fonctions se trouvant dans le fichier linked-list.c.
Création d’une bibliothèque
- Bien sûr, il faut d’abord créer les fichiers objets, par exemple, ici : gcc -Wall -c linked-list.c
- Création de l’archive en utilisant la commande ar : ar -r libmy.a fic1.o fic2.o
- L’option -r permet d’inclure les fichiers objets fic1.o et fic2.o dans l’archive nommée libmy.a. Notez que les bibliothèques commencent le plus souvent par le préfix lib.
- L’option -t affiche la liste des fichiers contenus dans l’archive
$ ar -r liblinked_list_utils.a linked-list.o ar: creating archive liblinked_list_utils.a $ ar -t liblinked_list_utils.a linked-list.o
- Gestion de la bibliothèque.
- La commande nm permet de vérifier quels sont les symboles définis et indéfinis dans l’archive.
- Pour ajouter d’autres objets dans une bibliothèque , on utilise la commande ar avec l’option -r.
Exercice 9: créez la bibliothèque liblinked_list_utils.a à partir du fichier objet linked-list.o
Nous n’avons plus besoin du fichier objet, et nous supprimons aussi le fichier executable; faites :
$rm linked-list.o linked-list-exe
Utilisation d’une bibliothèque
Vous allez maintenant faire l’édition de lien en utilisant la bibliothèque:
$ gcc main.o liblinked_list_utils.a -o linked-list-exe
- On peut aussi utiliser l’option -l de l’éditeur de liens pour lui demander d’aller chercher une bibliothèque. Cette option s’utilise ainsi : -lx où x est une abréviation pour libx.a. C’est pour cette raison que nous avons utilisé le nom liblinked_list_utils.a dans l’exemple. On peut donc utiliser cette bibliothèque en faisant :
$gcc main.o -L. -llinked_list_utils -o linked-list-exe
- Si l’éditeur de liens ne trouve pas la bibliothèque, il faut lui indiquer qu’elle est dans le répertoire courant en utilisant l’option -L (suivi du chemin vers le répertoire contenant la bibliothèque) :
$gcc main.o -L. -llinked_list_utils -o linked-list-exe
Exercice 10: créez l’exécutable linked-list-exe par édition de lien entre main.o et liblinked-list-utils.a
- On va maintenant mettre la bibliothèque libma_bib.a dans le répertoire lib : $mkdir lib $mv liblinked_list_utils.a ./lib/liblinked_list_utils.a L’option -Lrep_bib de gcc indique qu’il faut chercher les bibliothèques dans le répertoire rep_bib. La commande à exécuter est alors : $gcc main.o -L./lib -llinked_list_utils -o appli
Exercice 11: Après avoir déplacé la bibliothèque dans le répertoire lib, re-créez l’exécutable linked-list-exe par édition de lien entre main.o et liblinked-list-utils.a
Informations complémentaires sur les bibliothèques:
Les bibliothèques présentées dans ce TP sont des bibliothèques statiques. Les bibliothèques dynamiques sont construites par gcc (en utilisant l’option -shared). Les bibliothèques dynamique ont une extension « .so » et exposent plus de symboles et d’informations qu’une bibliothèques statique pour pouvoir être chargé dynamiquement.
Certaines bibliothèques, en particulier la « libc », sont ajoutées par défaut lors de l’édition de lien (voir utilisation de la commande ldd plus haut dans ce TP). C’est ainsi que sont résolus les symboles scanf, printf, etc.
Lorsque vous utilisez des opérations mathématiques (e.g. sqrt), vous pouvez utiliser la librairie mathématique. Pour cela, il vous suffit d’utiliser l’option -lm, ce qui signifie que gcc trouvera un fichier libm.a (ou libm.so). Où? … Dans /usr/lib
Pour information : annexe, illustrée sur un autre exemple que celui du TP
Pour aller plus loin, faites les exercices qui suivent; il n’est pas obligatoire de finir la partie du TP ci-dessous.
Outil de debug (GDB)
Pour les mises au point de programmes (i.e. le debug), il est conseillé d’utiliser des outils tels que gdb qui permettent de suivre l’exécution du programme en pas à pas et de vérifier ou de modifier le contenu de leurs variables.
Souvent plus efficace que le mise au point faite en utilisant printf !
Si vous ne l’avez pas déja fait, ajoutez l’option -g aux paramétres CFLAGS et LDFLAGS.
Faites make clean puis make all, puis utilisez gdb en vous inspirant de l’exemple ci-dessous.
Exemple :
Pour utiliser gdb sur le programme appli, on entre la commande suivante :
gdb appli
On peut maintenant exécuter ce programme appli sous le contrôle de gdb, et essayer les commandes suivantes :
- list affiche 10 lignes à partir de la position courante,
- break positionne un point d’arrêt,
- run démarre l’exécution
- continue relance l’exécution jusqu’au point d’arrêt suivant,
- step fait passer à l’instruction suivante,
- print visualise la valeur des variables,
- set permet de modifier la valeur d’une variable,
- help donne le liste des commandes,
Par exemple :
(gdb) list 25 20 21 printf ("nom du fichier ?\n"); 22 scanf( "%s", le_fichier); 23 24 nb_mots = lire_fichier(tab_mots, le_fichier); 25 26 /*-- visualisation du tableau a l ecran ---*/ 27 for (i=0; i<=nb_mots; i++){ 28 printf("tab_mots[%d] = %s %d\n", i, tab_mots[i].texte, 29 tab_mots[i].info); (gdb) break 27 Breakpoint 1 at 0x400758: file main.c, line 27. (gdb) run Starting program: appli nom du fichier ? main.c Breakpoint 1, main (argc=1, argv=0x7fffffffde98) at main.c:27 27 for (i=0; i<=nb_mots; i++){ (gdb) print nb_mots $1 = 98 (gdb) set nb_mots=10 (gdb) print nb_mots $3 = 10
Structures des fichiers objets et exécutables
Un premier fichier objet
Exécuter la commande suivante :
- objdump -h main.o (si objdump n’existe pas, essayer gobjdump).
On obtient des informations sur les différentes sections du fichier objet main.o, en particulier :
Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000148 0000000000000000 0000000000000000 00000040 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1 .data 00000000 0000000000000000 0000000000000000 00000188 2**2 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000000 0000000000000000 0000000000000000 00000188 2**2 ALLOC 3 .rodata 00000068 0000000000000000 0000000000000000 00000188 2**0
Commentaires :
- size et File off : taille de la section et son adresse de début dans le fichier objet,
- Algn : alignement, en puissance de deux,
- la section text contient le programme,
- la section data contient les données modifiables ,
- la section rodata contient les données non modifiables (read only!), dans notre cas les chaines de caractères des formats utilisés dans les printf.
Exécuter la commande suivante :
- objdump -t main.o
Résultat :
main.o: file format elf64-x86-64 SYMBOL TABLE: 0000000000000000 l df *ABS* 0000000000000000 main.c 0000000000000000 l d .text 0000000000000000 .text 0000000000000000 l d .data 0000000000000000 .data 0000000000000000 l d .bss 0000000000000000 .bss 0000000000000000 l d .rodata 0000000000000000 .rodata 0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack 0000000000000000 l d .eh_frame 0000000000000000 .eh_frame 0000000000000000 l d .comment 0000000000000000 .comment 0000000000000e10 O *COM* 0000000000000020 tab_mots 0000000000000000 g F .text 0000000000000148 main 0000000000000000 *UND* 0000000000000000 puts 0000000000000000 *UND* 0000000000000000 __isoc99_scanf 0000000000000000 *UND* 0000000000000000 lire_fichier 0000000000000000 *UND* 0000000000000000 printf 0000000000000000 *UND* 0000000000000000 strncmp 0000000000000000 *UND* 0000000000000000 chercher
Commentaires sur le résultat de cette commande :
- COM : variable globale non initialisée,
- data : variable globale initialisée,
- UND : référence non satisfaite,
- la première colonne indique l’adresse de la variable dans sa section, la troisième sa taille, par exemple : 0000000000000e10 O *COM* 0000000000000020 tab_mots veut dire que la variable tab_mots est implantée sur 20 octets à partir de l’adresse e10 dans la zone COM.
Si on exécuter la commande :
- objdump -s main.o
On peut voir le contenu de cette section rodata:
Contents of section .rodata: 0000 6e6f6d20 64752066 69636869 6572203f nom du fichier ? 0010 00257300 7461625f 6d6f7473 5b25645d .%s.tab_mots[%d] 0020 203d2025 73202564 0a007175 656c206d = %s %d..quel m 0030 6f742063 68657263 68657220 3f006669 ot chercher ?.fi 0040 6e007472 6f757665 20202573 20286d6f n.trouve %s (mo 0050 74202564 2920210a 00706173 2074726f t %d) !..pas tro 0060 75766520 25730a00 uve %s..
Un second fichier objet
Exécuter la commande suivante :
- objdump -h foncs.o
On obtient des informations sur les différentes sections du fichier objet foncs.o:
Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000139 0000000000000000 0000000000000000 00000040 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1 .data 00000000 0000000000000000 0000000000000000 0000017c 2**2 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000000 0000000000000000 0000000000000000 0000017c 2**2 ALLOC 3 .rodata 00000040 0000000000000000 0000000000000000 0000017c 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA
Exécuter la commande suivante :
- objdump -t foncs.o
Extrait du résultat :
SYMBOL TABLE: 0000000000000000 l df *ABS* 0000000000000000 foncs.c 0000000000000000 l d .text 0000000000000000 .text 0000000000000000 l d .data 0000000000000000 .data 0000000000000000 l d .bss 0000000000000000 .bss 0000000000000000 l d .rodata 0000000000000000 .rodata 0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack 0000000000000000 l d .eh_frame 0000000000000000 .eh_frame 0000000000000000 l d .comment 0000000000000000 .comment 0000000000000000 g F .text 00000000000000c2 lire_fichier 0000000000000000 *UND* 0000000000000000 fopen 0000000000000000 *UND* 0000000000000000 printf 0000000000000000 *UND* 0000000000000000 exit 0000000000000000 *UND* 0000000000000000 __isoc99_fscanf 00000000000000c2 g F .text 0000000000000077 chercher 0000000000000000 *UND* 0000000000000000 tab_mots 0000000000000000 *UND* 0000000000000000 strcmp
Commentaires sur le résultat de cette commande :
- text contient lire_fichier et chercher qui sont UND dans main.o,
- toutes les autres variables sont UND .
Exécuter la commande suivante :
- objdump -s foncs.o
Extrait du résultat :
Contents of section .rodata: 0000 72006c65 20666963 68696572 20257320 r.le fichier %s 0010 6e276578 69737465 20706173 0a002573 n'existe pas..%s 0020 006c6967 6e652020 2564203a 20657272 .ligne %d : err 0030 65757220 0a000000 00000000 0000803f eur ...........?
Commentaires sur le résultat de cette commande :
- pas de data
- dans rodata on trouve touts les constantes chaines de caractères des formats utilisés dans les printf.
Le fichier exécutable
Produire un fichier exécutable en utilisant la commande suivante :
gcc main.o foncs.o -o exo
Exécuter ensuite la commande suivante :
- objdump -h exo
On obtient des informations sur les différentes sections du fichier exécutable exo, on s’intéresse aux suivantes:
Sections: 14 .rodata 000000b8 0000000000400a30 0000000000400a30 00000a30 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 23 .data 00000004 0000000000600e60 0000000000600e60 00000e60 2**2 CONTENTS, ALLOC, LOAD, DATA 24 .bss 00000e30 0000000000600e80 0000000000600e80 00000e64 2**5
Commentaires :
- la section rodata a une taille égale à la somme de celles de main.o et foncs.o, elle commanece en a30 dans l’exécutable.
- la section data contient toutes las variables globales initialisées,
- la section bss contient toutes les variables globales non initialisées.
La pile
- La pile croit dans le sens des adresses décroissantes.
- Rôle de %esp et %ebp : Les registres 32 bits %esp et %ebp définissent, dans la pile, l’espace de la fenêtre (stack frame) associée à une fonction :
- %ebp : pointeur sur la base de la pile,
- %esp : pointeur sur le sommet de la pile, modifié par les instructions pushl, popl, call, ret.
- Sauver/Restaurer : Sauver les valeurs de %esp et %ebp mémorise un contexte dans la pile. Restaurer les valeurs de ces registres permet de se retrouver dans le contexte sauvegardé.
- Mécanisme push/pull :
- pushl var : décrémente %esp et copie 4 octets,
- popl var : copie 4 octets dans var et incrémente %esp,
- call, ret et leave : Les trois instructions spécifiques aux appels de fonction sont:
- call Fonc call est l’enchainement d’un empilement de l’adresse qui la suit et d’un jmp à l’adresse fournie en opérande.
- leave restaure la fenetre de pile précédente, c.a.d :
movl %ebp,%esp popl %ebp
- ret retour de fonction (c.a.d : popl %eip). L’instruction ret combine dépilement et branchement à l’adresse dépilée.
- valeur de retour : L’éventuelle valeur de retour est rangee dans %eax.
- Etat de la pile dans la fonction : 4(%ebp) adresse de retour 8(%ebp) argument 1 12(%ebp) argument 2 …
Génération de code
On donne ici un fichier source écrit en langage C et le code assembleur produit par le compilateur gcc (3.4.2) sur un système Linux/PC Intel.
Remarquer les optimisations pour les cas factorielle(0) et factorielle(1).
/* * factorielle.c */ #include int main(void){ int factorielle (int n); static int n, i =4; n = factorielle(i); printf("Factorielle(%d) = %d\n", i, n); return 0; } int factorielle(int n){ int i; if ( (n == 1) || (n == 0) ) i= 1; else i = n * factorielle(n-1); return i; }
Le fichier assembleur (simplifié et commenté) produit par la commande : gcc -Wall -O4 -s factorielle.c
//////////////////////////////////////////////////// .comm n.0,4,4 i.1: .long 4 .LC0: .string "Factorielle(%d) = %d\n" .text main: pushl %ebp movl %esp, %ebp pushl %ebx pushl %ebx andl $-16, %esp movl i.1, %ebx mettre i dans ebx subl $16, %esp cmpl $1, %ebx comparer ebx avec 1 movl $1, %eax mettre 1 dans eax (valeur de retour) ja .L11 aller a L11 SEULEMENT SI i > 1 movl %eax, n.0 initialiser n pushl %edx pushl %eax mettre n sur la pile pushl i.1 mettre i sur la pile pushl $.LC0 mettre adresse chaine de carc. sur la pile call printf optimisation : appel direct a printf xorl %eax, %eax movl -4(%ebp), %ebx leave ret .L11: leal -1(%ebx), %eax decrementer ebx et le mettre dans eax (eax est aussi valeur de retour) pushl %eax mettre eax sur la pile comme argument call factorielle imull %ebx, %eax popl %ecx movl %eax, n.0 pushl %edx pushl %eax mettre n sur la pile pushl i.1 mettre i sur la pile pushl $.LC0 mettre adresse chaine de carc. sur la pile call printf xorl %eax, %eax movl -4(%ebp), %ebx leave ret factorielle: pushl %ebp decrementer esp et sauver ebp sur la pile movl %esp, %ebp mettre le esp courant dans ebp (nouvelle fenetre) pushl %ebx decrementer esp et sauver ebx sur la pile movl 8(%ebp), %ebx ebx = valeur du premier argument dans la fenetre precedente cmpl $1, %ebx comparaison avec 1 movl $1, %eax toujours mettre 1 dans eax ja .L6 si argument > 1 : aller a L6 movl -4(%ebp), %ebx (1) restaurer ebx leave restaurer la fenetre de pile précédente (c.a.d movl %ebp,%esp puis popl %ebp) ret depiler le compeur ordinal sauve (c.a.d : popl %eip) .L6: subl $12, %esp decrementer esp de 12 pour preparer une nouvelle fenetre leal -1(%ebx), %eax decrementer ebx et le mettre dans eax (valeur de retour) pushl %eax mettre eax sur la pile comme argument call factorielle appel avec (n-1) en argument imull %ebx, %eax ebx a ici la valeur deposee en (1) addl $16, %esp faire pointer esp sur la fenetre precedente movl -4(%ebp), %ebx restaurer ebx leave restaurer la fenetre de pile précédente ret