CSC 4103 – Programmation système

Portail informatique
Modularité

Avant de commencer ce TP, téléchargez l'archive TP3.tgz et extrayez la.

L'objectif de cet exercice est d'étudier le travail du préprocesseur et l'effet de quelques directives.

Pour commencer cet exercice, placez vous dans le répertoire 1-preprocesseur.

Compilez le fichier hello.c avec la commande gcc -c hello.c. Vous devriez obtenir des erreurs de compilation.

$ gcc -c hello.c hello.c: In function ‘main’: hello.c:4:11: error: expected identifier or ‘(’ before numeric constant #define N 12 ^ hello.c:8:7: note: in expansion of macro ‘N’ int N; ^ hello.c:9:5: error: lvalue required as left operand of assignment N = 7; ^
Pour comprendre ces erreurs, compilez le fichier hello.c avec la commande gcc -E hello.c. L'option -E permet d'arrêter gcc après le travail du préprocesseur. Vous pouvez remarquer le travail de substitution du préprocesseur. Observez la manière dont les mentions à N dans hello.c ont été remplacées par le pré-processeur. A quel(s) endroit(s) avez-vous besoin de remplacer N par n pour que hello.c compile. Compilez pour vérifier votre hypothèse. $ gcc -E hello.c [...] # 6 "hello.c" int main() { int grandN=15; int 12; 12 = 7; return 0; }
Étudiez le programme condition.c, puis générez l'exécutable condition et exécutez le. $ gcc -o condition condition.c $ ./condition N est très grand (12) Compilez maintenant le programme condition.c en ajoutant l'option -DN=7. Exécutez le programme et observez le résultat. $ ./condition N est très moyen (7)

On souhaite maintenant que N ait la même valeur que M si M est défini. Si M n'est pas défini, N conserve sa valeur.

Modifiez le programme condition.c et testez votre implémentation

$ gcc -o condition condition.c $ ./condition N est très grand (12) $ gcc -o condition condition.c -DM=5 $ ./condition N est très moyen (5)

Étudiez le programme tab.c, puis essayez de générer le fichier objet tab.o.

Quelle est la cause de l'erreur ?

$ gcc -c tab.c In file included from tab.h:1:0, from tab.c:3: types.h:2:8: error: redefinition of ‘struct tableau’ struct tableau { ^~~~~~~ In file included from tab.c:2:0: types.h:2:8: note: originally defined here struct tableau { ^~~~~~~ L'erreur est causée par une double inclusion du fichier types.h (inclus dans tab.c et dans tab.h). $ gcc -E tab.c [...] # 2 "types.h" struct tableau { int size; int* values; }; # 3 "tab.c" 2 # 1 "tab.h" 1 # 1 "types.h" 1 struct tableau { int size; int* values; }; # 2 "tab.h" 2 void saisir_tab(struct tableau*tab); void afficher_tab(struct tableau*tab); # 4 "tab.c" 2 void saisir_tab(struct tableau*tab) { int i; for(i=0; isize; i++) { printf("valeur ?"); scanf("%d", &tab->values[i]); } } void afficher_tab(struct tableau*tab) { int i; for(i=0; isize; i++) { printf("%3d ", tab->values[i]); } printf("\n"); }

À l'aide de directives #ifndef et #define, implémentez une solution à ce problème :

  • Ajoutez les lignes suivantes au début du fichier types.h: #ifndef _TYPES_H #define _TYPES_H
  • Mettez la ligne suivante à la fin de votre fichier types.h: #endif /* _TYPES_H */
  • Reproduisez ces deux opérations pour le fichier tab.h en utilisant _TAB_H
Vérifiez que le programme compile désormais. Expliquez pourquoi. Cette démarche est très classique (cf., par exemple, le fichier /usr/include/unistd.h). Nous vous recommandons de systématiquement l'appliquer aux fichiers .h que vous écrivez.

La solution consiste à ajouter des "#include guards" évitant les inclusions multiples de fichiers .h.

Il s'agit d'une construction qu'habituellement, on ajoute à tous les fichiers .h.

L'objectif de cet exercice est de se familiariser avec la compilation de modules en C. Pour commencer cet exercice, placez vous dans le répertoire 2-modules Créez le fichier objet main_tab.o à partir du fichier main_tab.c. $ gcc -c main_tab.c $ ls main_tab.c main_tab.o tab.c tab.h À partir de l'objet main_tab.o, essayez de générer l'exécutable main_tab. À quelle étape de la compilation a lieu l'erreur ? $ gcc -o main_tab main_tab.o main_tab.o : Dans la fonction « main » : main_tab.c:(.text+0x1c) : référence indéfinie vers « saisir_tab » main_tab.c:(.text+0x2d) : référence indéfinie vers « afficher_tab » collect2: error: ld returned 1 exit status L'erreur a lieu lors de l'édition de liens. Utilisez la commande nm pour retrouver les fonctions définies dans l'objet main_tab.o ainsi que les fonctions qui y sont utilisées. $ nm main_tab.o U afficher_tab 0000000000000000 T main U saisir_tab Donc main_tab.o définit la fonction main et utilise les fonctions afficher_tab et saisir_tab. Les fonctions utilisées par main_tab.c sont définies dans tab.c. Générez l'objet tab.o à partir de tab.c, puis l'exécutable main_tab à partir de tab.o et main_tab.o. $ gcc -c tab.c $ gcc tab.o main_tab.o -o main_tab Utilisez la commande nm pour examiner les symboles de l'exécutable main_tab. L'exécutable contient plusieurs fonctions qui ne sont présentes ni dans tab.c ni dans main_tab.c. À quoi correspondent ces fonctions bizarres ? $ nm main_tab 0000000000400602 T afficher_tab 0000000000600af0 B __bss_start 0000000000600af0 b completed.6979 0000000000600ae0 D __data_start 0000000000600ae0 W data_start 00000000004004e0 t deregister_tm_clones 0000000000400560 t __do_global_dtors_aux 00000000006008c0 t __do_global_dtors_aux_fini_array_entry 0000000000600ae8 D __dso_handle 00000000006008d0 d _DYNAMIC 0000000000600af0 D _edata 0000000000600af8 B _end 0000000000400714 T _fini 0000000000400580 t frame_dummy 00000000006008b8 t __frame_dummy_init_array_entry 00000000004008b0 r __FRAME_END__ 0000000000600aa8 d _GLOBAL_OFFSET_TABLE_ w __gmon_start__ 0000000000400738 r __GNU_EH_FRAME_HDR 0000000000400428 T _init 00000000006008c0 t __init_array_end 00000000006008b8 t __init_array_start 0000000000400720 R _IO_stdin_used U __isoc99_scanf@@GLIBC_2.7 w _ITM_deregisterTMCloneTable w _ITM_registerTMCloneTable 00000000006008c8 d __JCR_END__ 00000000006008c8 d __JCR_LIST__ w _Jv_RegisterClasses 0000000000400710 T __libc_csu_fini 00000000004006a0 T __libc_csu_init U __libc_start_main@@GLIBC_2.2.5 000000000040065a T main U printf@@GLIBC_2.2.5 U putchar@@GLIBC_2.2.5 0000000000400520 t register_tm_clones 00000000004005a6 T saisir_tab 00000000004004b0 T _start 0000000000600af0 D __TMC_END__ Les "symboles bizarres" sont ajoutés automatiquement par l'éditeur de liens. Ils sont utilisés par l'OS lors du lancement de l'application. Par exemple, la fonction _start est chargée par le lanceur d'initialiser le processus. Le but de cet exercice est de comprendre la portées des variables déclarées dans un fichier.

Pour cet exercice, placez vous dans le répertoire 4-portee_variables.

Dans le fichier foo.c, écrivez la fonction int main(int argc, char**argv). Complétez cette fonction pour qu'elle déclare une variable entière locale et une variable entière locale et statique. Lors de leur déclaration, initialisez ces variables (à 0 par exemple).

Complétez la fonction pour qu'elle affiche la somme des deux variables.

Générez, à partir de foo.c, l'objet foo.o. Affichez la liste des symboles de l'objet. Que remarquez vous ?

$ gcc -c foo.c $ nm foo.o 0000000000000000 T main U printf 0000000000000000 b var_locale_statique.2213
  • la variable locale (var_locale) n'apparaît pas.
  • la variable statique locale (var_locale_statique) apparaît dans la section des données non initialisées (b) et est locale à l'objet (car b est en minuscule).
Dans foo.c, déclarez maintenant trois variables de type int:
  • Une variable globale nommée var_globale
  • Une variable globale statique nommée var_globale_static
  • Une variable globale externe nommée var_globale_extern
Initialisez les variables (enfin, celles qui peuvent l'être), et modifiez la fonction main pour qu'elle utilise les trois nouvelles variables.
Générez foo.o et observez les symboles de l'objet. $ gcc -c foo.c $ nm foo.o 0000000000000000 T main U printf 0000000000000000 B var_globale U var_globale_extern 0000000000000004 b var_globale_static 0000000000000008 b var_locale_statique.2216 On voit les 3 variables globales:
  • var_globale est stockée dans la section des données non initialisées et est globale (B majuscule)
  • var_globale_static est également stockée dans la section des données non initialisées, mais est locale (b minuscule)
  • var_globale_extern est "undefined"
Essayez de créer l'exécutable foo à partir de foo.o. D'où vient l'erreur ? $ gcc -o foo foo.o foo.o : Dans la fonction « main » : foo.c:(.text+0x33) : référence indéfinie vers « var_globale_extern » collect2: error: ld returned 1 exit status La variable var_globale_extern n'est pas défini. Comme elle a été déclarée extern, seul son "prototype" est connu. La variable n'est pas allouée dans foo.o.

Créez le fichier bar.c, et déclarez-y une variable globale de type int nommée var_globale_extern.

Créez bar.o, puis l'exécutable main à partir de foo.o et bar.o. Le programme peut-il s'exécuter ? Cela fonctionne-t-il si var_globale_extern est déclarée static ? Pourquoi ? $ gcc -c bar.c $ gcc -o foo foo.o bar.o $ ./foo 0 Ca fonctionne ! # avec static int var_globale_extern $ gcc -c bar.c $ gcc -o foo foo.o bar.o foo.o : Dans la fonction « main » : foo.c:(.text+0x33) : référence indéfinie vers « var_globale_extern » collect2: error: ld returned 1 exit status Ca ne fonctionne pas. var_global_extern étant déclarée static, cette variable n'est utilisable que depuis l'intérieur de bar.o.
Le but de cet exercice est d'apprendre à créer et à utiliser des bibliothèques dynamiques. Pour commencer cet exercice, placez vous dans le répertoire 3-bibliotheque

On souhaite créer la bibliothèque libtab.so à partir de tab.c (situé dans le répertoire 3-bibliotheque/tab/).

Commencez par générer tab.o avec la commande gcc -c tab.c. Utilisez ensuite le fichier objet créé pour générer libtab.so. Que signifie le message d'erreur que vous obtenez ?

$ gcc -c tab.c $ gcc -shared -o libtab.so tab.o /usr/bin/ld: tab.o: réadressage de R_X86_64_32 en vertu de « .rodata » ne peut être utilisé lors de la création d'un objet partagé; recompilez avec -fPIC tab.o : erreur lors de l'ajout de symboles : Mauvaise valeur collect2: error: ld returned 1 exit status L'objet tab.o n'a pas été créé avec l'option -fPIC. Il ne peut donc pas être intégré dans une bibliothèque dynamique.
Regénérez maintenant tab.o en utilisant la bonne commande, puis créez la bibliothèque partagée libtab.so. $ gcc -c tab.c -fPIC $ gcc -shared -o libtab.so tab.o Vérifiez avec file que le fichier généré est bien une bibliothèque partagée. $ file libtab.so libtab.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=77e40c12350bc88f253e5df5161ba00e9e5cd6f6, not stripped Le fichier est bien un "shared object" (d'où l'extension .so sous Linux) Dans le répertoire 3-bibliotheque/main, créez l'objet main_tab.o à partir du programme main_tab.c. Quelle option devez vous ajouter pour que le compilateur puisse créer l'objet ? $ gcc -c main_tab.c main_tab.c:2:17: fatal error: tab.h: Aucun fichier ou dossier de ce type #include "tab.h" ^ compilation terminated. $ cd ../main/^C $ gcc -c main_tab.c -I../tab Il est nécessaire d'ajouter l'option -I../tab qui indique au compilateur où chercher les fichiers .h Créez maintenant l'exécutable main_tab à partir de main_tab.o et de la bibliothèque libtab.so. Quelles options devez vous ajouter pour que l'édition de liens fonctionne ? $ gcc -o main_tab main_tab.o -ltab -L../tab

Il faut ajouter l'option -ltab qui indique à l'éditeur de liens que la bibliothèque libtab.so (ou libtab.a) est nécessaire.

Il est également nécessaire d'ajouter l'option -L../tab qui indique à l'éditeur de liens où chercher les bibliothèques.

Essayez d'exécuter main_tab. Que remarquez vous ? $ ./main_tab ./main_tab: error while loading shared libraries: libtab.so: cannot open shared object file: No such file or directory Le programme ne s'exécute pas car libtab.so est introuvable. Utilisez l'outil ldd pour afficher la liste des "dépendances" de l'exécutable main_tab. $ ldd main_tab linux-vdso.so.1 (0x00007ffca7fda000) libtab.so => not found libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5d2c899000) /lib64/ld-linux-x86-64.so.2 (0x0000560e280b6000)

Modifiez maintenant la variable d'environnement LD_LIBRARY_PATH afin de pouvoir exécuter le programme. Vous pouvez également vérifier avec ldd que libtab.so est bien trouvée.

NB: le sujet de cet exercice est prévu pour un système Linux. Si vous êtes utilisateur d'un Mac (personne n'est parfait), modifiez la variable d'environnement DYLD_LIBRARY_PATH, qui est l'équivalent de LD_LIBRARY_PATH sous Mac.

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../tab $ ./main_tab valeur ? [...] $ ldd main_tab linux-vdso.so.1 (0x00007ffc1c553000) libtab.so => ../tab/libtab.so (0x00007f6651b49000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6651774000) /lib64/ld-linux-x86-64.so.2 (0x000055b8da0da000)
Le but de cet exercice est d'apprendre à écrire des Makefile pour vos projets en C.

Pour cet exercice, placez vous dans le répertoire 5-makefile.

On souhaite, dans un premier temps, automatiser la compilation du programme main.c situé dans le répertoire src/.

Écrivez le fichier src/Makefile pour qu'il permette de générer l'exécutable main à partir de main.c et foo.c.

À l'aide de la commande make, générez l'exécutable main. Vérifiez que les dépendances sont correctement prises en compte en cas de recompilation: seuls les fichiers ayant besoin d'être regénérés sont recréés. $ make cc -c main.c cc -c foo.c cc -o main main.o foo.o $ make make: rien à faire pour « all ». # après modification de main.c $ make cc -c main.c cc -o main main.o foo.o Ici, on voit que foo.c n'est pas recompilé. Ajoutez une règle clean au Makefile permettant de "faire le ménage" (supprimer le fichier main et les fichiers *.o).

Afin de contrôler facilement les options passées au compilateur lors de la création des fichiers objets, créez une variable CFLAGS et initialisez la à -Wall -Werror.

Adaptez votre Makefile pour qu'il utilise cette variable.

$ make gcc -c main.c -Wall -Werror gcc -c foo.c -Wall -Werror gcc -o main main.o foo.o
Dans le répertoire libtab, créez le Makefile permettant de générer libtab.so à partir de tab.c et tab.h.

Dans src, modifiez main.c pour qu'il utilise la fonction void saisir_tab(int*tab, int size) définie dans libtab/tab.c.

Modifiez ensuite le Makefile pour permettre de générer l'exécutable main.

La solution de compilation actuelle a pour inconvénient que si l'on modifie tab.c (ou tab.h), il est nécessaire de lancer la commande make dans libtab, puis dans src.

Modifiez le Makefile afin de corriger ce problème. Vous pouvez pour cela utilise make -C <directory> qui lance récursivement la commande make dans le répertoire directory.

Cette solution n'est pas parfaite puisque l'exécutable main est regénéré systématiquement (même si libtab.so n'a pas changé). Dans la "vraie vie", on utilise des solutions plus évoluées, par exemple en utilisant automake ou Cmake.
L'objectif de ces différents exercices est d'illustrer avec des codes simples les différentes notions vues dans ce chapitre.

Avant de commencer ces exercices, créez-vous un répertoire balade dans lequel vous travaillerez.

Créez un fichier main.c avec le code suivant :

int main() { puts("Hello world !"); return 0; }

cc -Wall -o executable main.c # On fait exprès de ne pas mettre -Werror pour permettre la génération d'un exécutable

Ce code compile avec warning, mais ne cause pas d'erreur à l'exécution.

Morale de cette question : un code peut compiler avec warning et pourtant fonctionner.

Dans main.c, remplacez "Hello World !" par 42. Puis, compilez et exécutez : même warning à la compilation, sauf qu'on a une erreur de segmentation à l'exécution.

int main() { puts(42); // Ligne modifiée return 0; }

Morale de cette question : un code peut compiler avec le même warning que précédemment et pourtant ne plus fonctionner.

Corrigeons le warning pour voir si ça n'aiderait pas à comprendre l'erreur à l'exécution. man puts permet de voir la déclaration de puts que nous ajoutons au début du fichier main.c

int puts(const char *s); // Nouvelle ligne int main() { puts(42); return 0; }

À la compilation, nous obtenons un warning plus précis : il permet de comprendre que nous avons un souci de type au niveau de puts. Nous avons toujours une erreur à l'exécution (car nous n'écoutons pas le warning !)

Pour corriger le warning, castez l'entier 42 en char * de sorte que puts a effectivement une donnée de type char * en paramètre.

int puts(const char *s); int main() { puts((char*)42); // Ligne modifiée return 0; }

Le warning est corrigé, mais erreur à l'exécution !

Morale de cette question : faire un cast est une opération toujours délicate. Réfléchissez avant de le faire et surtout vérifiez que vous comprenez ce que vous faites.

Au lieu de caster 42 en char *, remplacez l'entier 42 par la chaîne de caractères "42".

int puts(const char *s); int main() { puts("42"); // Ligne modifiée return 0; }

Le programme fonctionne !

Morale de cette question : les warnings, c'est mal. Ne laissez aucun warning dans votre code, sous aucun prétexte. C'est d'ailleurs pour ça que nous vous demandons d'ajouter systématiquement l'option de compilation -Werror pour que vous soyez sûr de ne pas générer un exécutable en cas de warning.

La déclaration int puts(const char *s); améliore la qualité du code développé jusqu'à maintenant, c'est-à-dire qu'elle lui rajoute des informations pour que le travail de développement se fasse avec plus de garde-fous (ce qui est bien). Mais, en même temps, elle "pollue" notre code en le compliquant (ce qui est mal). En effet, elle rajoute une ligne qui ne sert pas au fonctionnement de notre code.

Vous trouvez que cette histoire de "pollution" est exagérée ? Hé bien, imaginez que votre code ait besoin d'utiliser également putchar (cf. man puts). Vous auriez alors besoin de rajouter la ligne int putchar(int c); pour ajouter un garde-fou à votre code.

Pour alléger votre code et le rendre plus lisible, remplacez int puts(const char *s); par #include <stdio.h>. Ainsi, au moment de la compilation, le compilateur (plus précisément le préprocesseur) remplace ce #include par le contenu du fichier stdio.h qui contient la déclaration int puts(const char *s);

#include <stdio.h> // Ligne modifiée int main() { puts("42"); return 0; }

Vérifiez que ça marche en compilant et en exécutant votre code.

Ensuite, pour voir ce remplacement en action, tapez la commande : cc -Wall -o resultatPreprocessing -E main.c

La compilation s'arrête à la phase de préprocessing dont le résultat (output) est stocké dans le fichier resultatPreprocessing qui est un source C. Faire more resultatPreporcessing, puis tapez /puts pour retrouver la déclaration de puts (notez le mot-clé extern dont nous parlerons plus tard dans cet exercice). Appuyez plusieurs fois sur la barre d'espace pour afficher la fin de votre fichier.

Conclusion : l'utilisation de #include rend le code plus lisible, tout en ajoutant des garde–fous.

Revenons au message "Hello world!" que nous affichions initialement. Comment faire pour que notre programme affiche ce message soit en anglais, soit en français ? Une solution possible est d'utiliser la compilation conditionnelle pour générer un code avec messages en anglais ou un code avec messages en français.

Modifiez l'instruction puts de la manière suivante :

puts( #ifdef FRANCAIS "Salut le monde !" #else "Hello world!" #endif ); #include <stdio.h> // Ligne modifiée

Puis, compilez (sans l'option -E). Enfin, exécutez pour vérifier que l'affichage est en anglais.

Compilez maintenant avec cc -Wall -o executable -DFRANCAIS main.c et exécutez pour vérifier que l'affichage est en français.

Arrêtez-vous à l'étape du préprocessing avec -E (en pensant à mettre -o resultatPreprocessing et non -o executable) et faites tail resultatPreprocessing. Voyez comment toutes les lignes commençant par # ont été remplacées par des lignes vides.

Morale de cette question : la compilation conditionnelle permet d'obtenir des exécutables différents à partir d'un même source. Cette technique est très utilisée pour assurer la portabilité d'un code entre différentes plateformes matérielles. Notez que pour la traduction de messages, les développeurs préfèrent d'autres techniques qui sortent du contexte de cette balade.

Le code précédent n'est pas très lisible avec son #ifdef. Voici une piste d'amélioration :

  • modifiez le code en écrivant simplement puts(MESSAGE);
  • compilez avec la commande cc -Wall -o executable -DMESSAGE='"Bonjour le monde !"' main.c (notez les quotes et les double quotes).

Compilez sans -E pour vérifier que ce code affiche le résultat attendu, puis avec -E pour voir le fruit du travail du préprocesseur.

Lors de nos digressions autour du préprocesseur, nous avons généré plusieurs exécutables : un pour avoir des messages en anglais, et l'autre pour avoir des messages en français. Voyons comment avoir un seul et même exécutable capable d'afficher de l'anglais ou du français.

Déclarez une variable message :

char *message[] = { "Hello World!", "Bonjour le monde !" };

et écrivons puts(message[maisQueMettreIci]);

Au niveau de maisQueMettreIci, nous pourrions coder un #define. Mais alors, nous obtiendrions à nouveau 2 exécutables.

Sur votre shell, tapez la commande env | grep LANG

Votre ordinateur devrait afficher au moins la ligne LANG=fr_FR.UTF-8

Dit autrement, la variable d'environnement LANG vous permet de savoir en quelle langue affiche votre ordinateur. Exploitons cela en écrivant :

int quelleEstMaLangue() { if (strcmp(getenv("LANG"),"fr_FR.UTF-8") == 0) { return 1; } else { return 0; } }

et puts(message[quelleEstMaLangue()]);

et enfin les 2 #include (stdlib.h et string.h) pour que tout cela compile sans warning.

Avec la question précédente, le code de main.c est complexifié par la présence du code pour le multi-langue. Aussi, nous allons mettre ce code à part.

Créez un fichier message.c

#include <stdlib.h> #include <string.h> char *message[] = { "Hello World!", "Bonjour le monde !" }; int quelleEstMaLangue() { if (strcmp(getenv("LANG"),"fr_FR.UTF-8") == 0) { return 1; } else { return 0; } }

main.c ne contient alors plus que :

#include <stdio.h> int main() { puts(message[quelleEstMaLangue()]); return 0; }

La ligne de compilation devient cc -Wall -o executable main.c message.c

Elle génère 2 warnings, mais l'exécutable donne le résultat attendu.

Puisque nous avons vu précédemment que les warnings c'est le mal, supprimons les warnings de la question précédente.

Ajoutez au début de main.c les lignes extern char *message[]; et extern int quelleEstMaLangue();

NB : il n'y a rien à faire sur le fichier message.c.

Vérifiez qu'il n'y a plus de warning à la compilation (et que l'exécution est correcte).

Nous sommes maintenant en présence d'une erreur potentielle difficile à détecter : imaginons que la personne en charge de coder la fonction quelleEstMaLangue() tape, dans message.c, float quelleEstMaLangue() { au lieu de int quelleEstMaLangue() {

cc -Wall -o executable main.c message.c : il n'y a aucun problème à la compilation.

Mais, à l'exécution, le message est en anglais au lieu d'être en français ! En effet, la valeur 1 est renvoyée par quelleEstMaLangue en tant que float dans message.c, mais cette valeur est interprétée comme l'entier de valeur 0 au niveau de main.c

Pour éviter ce problème d'incohérence entre déclarations et implémentations, nous ajoutons à message.c les lignes :

extern char *message[]; extern int quelleEstMaLangue();

Ainsi, à la compilation de message.c, nous obtenons un warning : nous sommes en mesure de détecter qu'il faut remplacer float quelleEstMaLangue() { par int quelleEstMaLangue() {. Corrigez votre code.

Pour ne pas avoir ces déclarations extern répétées (et donc source d'incohérences et donc d'erreurs potentielles), nous les centralisons dans message.h :

#ifndef _MESSAGE_H #define _MESSAGE_H extern char *message[]; extern int quelleEstMaLangue(); #endif /* _MESSAGE_H */

Vérifiez que la compilation se passe bien et que l'exécution est correcte.

Pour être propre au niveau de la structure des fichiers, nous mettons message.h dans un répertoire include qui est un sous-répertoire du répertoire courant.

La commande de compilation cc -Wall -o executable main.c message.c donne lieu à une erreur : il faut ajouter l'option -Iinclude pour indiquer au compilateur que des fichiers d'include se trouvent dans le répertoire include.

cc -Wall -o executable main.c message.c -Iinclude compile correctement.

Chronométrez la durée d'exécution de votre compilation en tapant la commande :

/usr/bin/time cc -Wall -o executable main.c message.c -Iinclude

L'elapsed est la durée de compilation.

Sur une machine de salle TP, ce temps est de 0.06 secondes. Il faut donc approximativement 0.03 secondes par fichier (nous verrons à la question suivante que cette approximation est inexacte, car elle ne distingue pas compilation et édition de lien).

0.03 secondes de compilation pour un fichier d'en moyenne 10 lignes. Sur un projet industriel, il est courant d'avoir 1000 fichiers d'environ 1000 lignes. Si nous devions compiler un tel projet, le temps de compilation serait de 0.03*(1000 lignes/10 lignes pour 0.03 secondes) * 1000 fichiers = 3000 secondes. Il est donc souhaitable de trouver une solution pour que notre compilation dure moins longtemps !

C'est pourquoi nous allons compiler nos fichiers séparément. Puis, nous mettrons le résultat de ces compilations ensemble.

cc -Wall -c -Iinclude main.c # Notez que pas d'option -o

cc -Wall -c -Iinclude message.c # Notez que pas d'option -o

cc main.o message.o -o executable # Notez que ni option -Wall, ni option -Iinclude

Les 2 premières commandes sont des commandes de compilation (elle convertissent chaque fichier source en fichier object). La dernière commande réalise l'édition de liens (elle regroupe les objets pour constituer un exécutable).

Pour recompiler juste ce qu'il faut, il est bienvenu de disposer d'un outil qui, en cas de changement, ne compile que ce qu'il y a besoin de compiler. C'est le rôle de make !

Mesurons le temps d'édition de liens : /usr/bin/time cc main.o message.o -o executable

Sur une machine de salle TP, l'édition de lien prend 0.02 secondes pour 2 fichiers. Pour 1000 fichiers de 1000 lignes, une approximation de ce temps est 0.01*(1000/10)*1000 fichiers = 1000 secondes.

C'est pourquoi les informaticiens ont créé la notion de bibliothèque pour réduire le temps d'édition de lien. En fait, vous utilisez des bibliothèques depuis que vous compilez des programmes C. Pour vous en rendre compte :

cc -v main.o message.o -o executable # Compilation verbeuse (avec plein d'informations)

qui fait apparaître la libc (à travers la chaîne de caractères -lc).

Le but de cet exercice est de comprendre comment fonctionnent les scripts de configuration (autoconf, automake) des "gros projets" en C.

Pour cet exercice, placez vous dans le répertoire 6-autotools.

Pour cet exercice, nous allons étudier un projet "de la vraie vie", mais qui reste petit. Il s'agit de LiTL (Lightweight Trace Library), un outil permettant d'enregistrer des traces de manière efficace.

Commencez par récupérer le code source du projet, puis affichez la liste des fichiers à la racine du projet

git clone http://fusionforge.int-evry.fr/anonscm/git/litl/litl.git $ git clone http://fusionforge.int-evry.fr/anonscm/git/litl/litl.git $ cd litl $ ls -l total 56 -rw-r--r-- 1 trahay trahay 159 nov. 22 09:00 AUTHORS -rwxr-xr-x 1 trahay trahay 27 nov. 22 09:00 bootstrap -rw-r--r-- 1 trahay trahay 441 nov. 22 09:00 ChangeLog -rw-r--r-- 1 trahay trahay 4004 nov. 22 09:00 configure.ac -rw-r--r-- 1 trahay trahay 1298 nov. 22 09:00 COPYING -rw-r--r-- 1 trahay trahay 1298 nov. 22 09:00 COPYRIGHT drwxr-xr-x 3 trahay trahay 4096 nov. 22 09:00 doc -rw-r--r-- 1 trahay trahay 209 nov. 22 09:00 litl.pc.in drwxr-xr-x 2 trahay trahay 4096 nov. 22 09:00 m4 -rw-r--r-- 1 trahay trahay 351 nov. 22 09:00 Makefile.am -rw-r--r-- 1 trahay trahay 0 nov. 22 09:00 NEWS -rw-r--r-- 1 trahay trahay 2006 nov. 22 09:00 README drwxr-xr-x 2 trahay trahay 4096 nov. 22 09:00 src drwxr-xr-x 2 trahay trahay 4096 nov. 22 09:00 tests drwxr-xr-x 2 trahay trahay 4096 nov. 22 09:00 utils

Parmi les fichiers, on remarque un fichier Makefile.am et un fichier configure.ac.

Le fichier configure.ac est utilisé par autoconf pour générer un script configure permettant de détecter la configuration de la machine sur laquelle installer le logiciel (emplacement des bibliothèques, choix des modules à activer, etc.)

Le fichier Makefile.am est utilisé par automake pour générer un fichier Makefile adapté à la configuration détectée par le script configure.

Ouvrez le fichier configure.ac et trouvez la ligne détectant si la fonction clock_gettime est disponible.

Ouvrez le fichier Makefile.am situé à la racine du projet. Vous verrez qu'il fait appel à des Makefile situés dans des sous-répertoires et n'est donc pas très intéressant. Ouvrez ensuite le fichier src/Makefile.am et trouvez la liste des fichiers utilisés pour générer liblitl.so.

Nous allons maintenant générer le script configure à partir de configure.ac. Lancez la commande suivante:

autoreconf -vfi

et observez les fichiers générez. Comparez le fichier configure.ac et le fichier configure.

Créez maintenant un répertoire build et un répertoire install. Le répertoire build va servir à compiler le logiciel, et install contiendra les fichiers installés.

Placez vous dans le répertoire build, lancez la configuration du logiciel, et observez.

../configure --prefix=$PWD/../install

L'option --prefix permet de spécifier le répertoire dans lequel sera installé le logiciel.

Consultez l'aide (configure --help) pour voir les autres options disponibles.

Une fois la configuration du logiciel terminée, observez les fichiers générés dans le répertoire build. Ouvrez le fichier build/src/Makefile et cherchez la règle pour construire liblitl.la

Lancez ensuite la compilation avec la commande make

Installez maintenant les fichiers compilez en lançant la commande make install

Observez le résultat dans le répertoire install

Utilisez la commande make check pour lancer les programmes de test. Cherchez dans tests/Makefile.am la liste des programmes de test.

Utilisez la commande make dist pour générer un tarball.

Félicitation ! Vous venez de générer votre premier tarball ! A vous la reconnaissance éternelle de la communauté open-source !!