CSC 4103 – Programmation système

Portail informatique

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

Travail du préprocesseur

Préprocesseur

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.

Compilation de modules

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.

Portée des variables

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 3-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.

Bibliothèques

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 4-bibliotheque

On souhaite créer la bibliothèque libtab.so à partir de tab.c (situé dans le répertoire 4-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.
Avec un compilateur récent, l'erreur n'apparait pas car le fichier .o est compilé par défaut avec l'option -fPIC.

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 4-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.

Rappelez vous vos cours de CSC3102 (Introduction aux systèmes d'exploitation), et exportez la variable LD_LIBRARY_PATH pour que votre modification soit prise en compte par les programmes que vous lancez:
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:nouveau_chemin

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 (attention, les répertoires sont séparés par , et non par :).

$ 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)

Makefile

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.

Balade

Warning et #include

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.

Compilation avec warning, mais le programme fonctionne

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.

Compilation avec warning, mais le programme ne fonctionne pas

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.

Correction du warning

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 !)

Correction du warning en faisant un cast

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.

Correction du warning de la bonne manière

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.

Intérêt des #include

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.

Digressions autour du préprocesseur

Compilation conditionnelle

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.

Stockage de sections de code dans la commande de compilation

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.

Modularité

Positionnons le problème

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.

Intérêt de modulariser un code

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> extern char *message[]; int main() { puts(message[quelleEstMaLangue()]); return 0; }

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

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

Déclaration d'extern dans un module utilisant du code d'un autre module

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

Ajoutez au début de main.c la ligne 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).

Possibilité d'avoir des bugs si les extern de variable ou de code n'apparaissent pas dans le module qui les définit

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

Création d'un fichier d'interfaces pour repérer ces problèmes dès la compilation

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.

Compilation séparée

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 !

Besoin de bibliothèques

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).

Pour aller plus loin: CMake

Le but de cet exercice est de comprendre comment fonctionne la configuration de projet avec CMake.

Pour cet exercice, créez le répertoire 6-cmake et déplacez vous y.

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 https://github.com/trahay/LiTL.git
$ git clone https://github.com/trahay/LiTL.git $ cd LiTL $ ls -l total 44 -rw-r--r-- 1 trahay trahay 159 16 déc. 16:55 AUTHORS -rw-r--r-- 1 trahay trahay 441 16 déc. 16:55 ChangeLog -rw-r--r-- 1 trahay trahay 1030 16 déc. 16:55 CMakeLists.txt -rw-r--r-- 1 trahay trahay 1298 16 déc. 16:55 COPYING -rw-r--r-- 1 trahay trahay 1298 16 déc. 16:55 COPYRIGHT drwxr-xr-x 3 trahay trahay 4096 16 déc. 16:55 doc -rw-r--r-- 1 trahay trahay 209 16 déc. 16:55 litl.pc.in -rw-r--r-- 1 trahay trahay 0 16 déc. 16:55 NEWS -rw-r--r-- 1 trahay trahay 2006 16 déc. 16:55 README drwxr-xr-x 2 trahay trahay 4096 16 déc. 16:55 src drwxr-xr-x 2 trahay trahay 4096 16 déc. 16:55 tests drwxr-xr-x 2 trahay trahay 4096 16 déc. 16:55 utils

Etude des fichiers CMake

Parmi les fichiers, on remarque un fichier CMakeLists.txt.

Commencez par ouvrir le fichier CMakeLists.txt, qui est utilisé par cmake pour configurer le projet. Le fichier décrit le projet (avec la commande project), liste les fonctionnalités optionnelles (commandes option), détecte les dépendances (CHECK_LIBRARY_EXISTS), et génère un fichier pour pkg-config (avec configure_file). Enfin, le fichier liste les sous-répertoires qui contiennent le code du projet (add_subdirectory).

Ouvrez maintenant le fichier src/CMakeLists.txt. Ce fichier décrit comment compiler la bibliothèque liblitl:

  • configure_file transforme le header litl_config.h.in en litl_config.h. Pour cela, il substitue les macros @TRUC@ par leurs valeurs telles que définies par cmake;
  • add_library liste les fichiers nécessaires pour créer la bibliothèque litl
  • install liste les objets qui doivent être installés lors de la phase d'installation.

Compilation du projet

Créez le sous-répertoire build et déplacez vous y. Lancez ensuite la commande:

cmake -DCMAKE_INSTALL_PREFIX=$PWD/../install ..

Cette commande demande à cmake de charger le fichier ../CMakeLists.txt et de l'exécuter. L'option CMAKE_INSTALL_PREFIX définit le répertoire où le projet doit être installé (par défaut, le projet est installé dans /usr/local, ce qui nécessite d'être administrateur de la machine).

Observez les fichiers qui ont été générés dans le répertoire courant (et ses sous répertoires

Compilez maintenant le projet avec la commande make, puis installez le avec make install. Observez les fichiers qui ont été installés dans le répertoire $PWD/../install.

Configuration du projet

Vous pouvez passer des options à cmake avec -DVARIABLE=VALEUR

Vous pouvez également configurer le projet avec une interface graphique en lançant: ccmake .. (vous aurez peut-être besoin d'installer le paquet cmake-curses-gui)

Contribution à un projet

Dans le projet LiTL, certaines fonctionnalités sont manquantes. Par exemple:

  • La documentation (dans le sous-répertoire doc) n'est pas générée. Ajoutez une option GENERATE_DOCUMENTATION qui, lorsqu'elle est activée vérifie si les dépendances (doxygen, pdflatex, latexmk) sont installées
  • Les tests (dans le sous-répertoire tests) ne sont pas compilé ni exécutés. Complétez le projet pour permettre l'exécution des tests avec make test

Si vous implémentez l'une où l'autre de ces fonctionnalités manquantes, n'hésitez pas à soumettre une merge requests pour contribuer au projet !