TP ‐ Environnement de Travail - vbridonneau/CoursSysteme GitHub Wiki
Utiliser la commande man
pour obtenir le manuel d'utilisation de
- la commande
gcc
- Chercher dans le manuel de cette commande le rôle jouer par les options
-Wall
et-Werr
. Vous ne devez pas utiliser les flèches directionnelles.
- Chercher dans le manuel de cette commande le rôle jouer par les options
- la commande
man
(oui le manuel a aussi un manuel d'utilisation). - la fonction de la bibliothèque standard C
printf
.- Que se passe-t-il si l'on se contente d'exécuter la commande suivante ?
man printf
- Que faut-il faire pour pallier ce problème ?
Utilisez la commande top
afin que celle-ci montre :
- Les programmes lancés par vous.
- Les performances de votre machine.
L'objectif de cet exercice est d'utiliser un programme de débogage afin de trouver l'origine d'un dysfonctionnement. Avec un tel programme, il est possible de :
- Marquer des points d'arrêts dans un programme. Ces points sont aussi appelés breakpoints dans la littérature.
- Afficher l'état d'une variable (sa valeur).
- Connaître l'état de la pile. Ceci est utile lorsque l'on veut savoir où l'on est dans un programme. On peut également connaître l'ensemble des fonctions appelées à un instant t.
- Avancer instruction par instruction dans un programme.
Le programme que l'on va utiliser pour déboguer nos programmes C se nomme gdb
.
Il s'utilise comme suit :
gdb [--args] ./programme [arguments] # En supposant que le programme se nomme `programme` dans le répertoire courant.
Prenez le programme C suivant:
#include <stdio.h>
#include <stdlib.h>
#define SIZE 10
void init(int* tab, int size) {
for (int i = 0; i <= size; i++) {
tab[i] = 0;
}
}
void print(int* tab, int size) {
for (int i = 0; i <= size; i++) {
printf("%d\n", tab[i]);
}
}
int main(int argc, char const** agrv) {
int *tab = malloc(SIZE * sizeof(int));
init(tab, SIZE);
print(tab, SIZE);
return 0;
}
Pour compiler un programme C afin de pouvoir le déboguer, il faut indiquer à gcc
quelques options supplémentaires.
En effet, gcc
ne fait pas que traduire votre code, il se permet également de l'optimiser s'il pense que certaines séquences d'instructions sont inutiles ou s'il pense pouvoir optimiser votre code.
Il faut donc demander à gcc
de ne pas faire cela.
- Cherchez dans le manuel quelles options fournir à
gcc
pour que le programme soit débogable.
Dans ce qui suit, nous allons utiliser gdb
afin de chercher des potentielles erreurs dans le code et naviguer dans celui-ci.
- Exécutez le programme sans le modifier. D'où vient l'erreur ?
Nous allons maintenant apprendre à naviguer dans un programme lors de son exécution ainsi qu'à lire le contenu de variables.
Pour cela reprenez le programme précédent et relancez le avec gdb
.
- Nous allons commencer par la navigation:
- Mettez un breakpoint à l'instruction précédant l'appel de la fonction
init
(breakpoint
oub
). - Mettez un breakpoint à l'entrée de la fonction
print
. - Lancez le programme (
run
). - Avancez d'une instruction pour arriver sur l'appel à la fonction
init
. - Entrez dans la fonction (
step
ous
). - Avancez de quelques instructions (
next
oun
). - Observez votre position dans le code (
where
) - Continuez l'exécution du programme (
continue
ouc
).
- Mettez un breakpoint à l'instruction précédant l'appel de la fonction
Une chose importante à remarquer est que les commandes step
et next
ont un comportement proche. Leur seul différence réside dans le fait qu'avec step
on peut rentrer à l'intérieur des fonctions.
- Nous allons maintenant apprendre à regarder le contenu de variables et à les modifiées. Relancez
gdb
comme précédemment puis:- Mettez un
breakpoint
au début de la fonctioninit
. - Avancez de quelques instructions afin d'être dans la boucle
for
. - Affichez le contenu de la variable
i
(print
oup
). - Changez le contenu de cette variable à 9 (
set variable i = 9
). - Continuer l'exécution du programme. Que se passe-t-il ?
- Mettez un
Notre objectif ici est d'automatiser la compilation d'un projet C ainsi que l'exécution et le débogage de ce projet. Pour cela nous allons créer un fichier appelé Makefile qui contiendra toutes les directives nécessaires à cette fin. L'objectif avec un tel fichier est de pouvoir réaliser la compilation, l'exécution ainsi que le débogage avec des commandes courtes et faciles à manipuler.
La compilation d'un projet se fera en lançant la commande :
make
L'exécution du projet se fera en lançant la commande :
make run
Le débogage du projet se fera en lançant la commande :
make debug
Dans cet exercice nous allons nous concentrer sur des Makefile pour des projets écrits en C.
De manière général un fichier Makefile pour des projets C commence très souvent par contenir les informations relatives au sources du projet (fichiers contenant le code de notre projet) ainsi que le nom de l'exécutable, le compilateur et ses options, les bibliothèques (-lm
pour les fonctions mathématiques).
SRC_DIR = # Le répertoire contenant les fichiers sources.
SRC= # Les sources.
OBJ_DIR = # Le répertoire contenant les fichiers sources.
OBJ= # Les fichiers objets (fichiers compilés).
EXEC= # Le nom de l'exécutable.
CC=gcc # Le compilateur.
FLAGS= # Les options du compilateur.
LIBS= # Les librairies (s'il y en a).
- Reprendre le code ci-dessus et le compléter pour qu'il contienne toutes les informations relatives au projet
-
Note : pour récupérer en une fois tous les fichiers sources possibles il est possible d'utiliser la commande
wildcard
. De la même manière pour convertir tous les fichiers d'extensions .c en .o on pourra utiliser la commandepatsubst
(ou sa cousinesubst
).
-
Note : pour récupérer en une fois tous les fichiers sources possibles il est possible d'utiliser la commande
Une fois que ces informations sont remplies, on peut reprendre notre Makefile pour écrire les différentes règles associées aux commandes que l'on souhaite voir implémentées. Dans notre cas il s'agit de la compilation, l'exécution et le débogage.
Dans un Makefile, une règle définit comment créer un ou plusieurs fichiers cibles à partir de fichiers sources. Une règle de base a la forme suivante :
cible: dependances
commandes
Si l'on souhaite créer le fichier cible à partir d'un terminal, il suffit de lancer make cible
et la cible sera exécuter.
Dans notre projet, toutes les sources sont des fichiers .c contenus dans le répertorie src. Les sources associées seront les mêmes fichiers mais avec l'extension .o. Il existe une façon simple de faire correspondre un nom du fichier .c avec un fichier .o avec l'aide du caractère %. C'est ce caractère qui permet d'être générique et de ne pas avoir à faire une règle par nom de fichier objet. Par exemple, le code suivant utilise ces concepts pour convertir des fichiers sources C en objet :
%.o: %.c
$(CC) ...
- Compléter et adapter ce code pour créer une règle générique permettant de créer un ficher objet à partir du fichier source qui lui correspond. Note : les fichiers sources sont dans un répertoire source et les objets seront placés dans un répertoire objet.
Maintenant que l'on a créé les fichiers objets, il va nous falloir les regrouper pour créer l'exécutable.
Pour cela on veut créer une règle qui, quand la commande make
sans argument est lancée compile notre projet.
Pour identifier ce cas précis, on définit une règle spéciale appelée all
.
- Écrire la règle
all
afin qu'elle compile l'ensemble des fichiers objets et crée l'exécutable. Note: on oubliera pas de mettre les dépendances vers les fichiers objets.
Dans ce petit exercice, on se propose d'utiliser un outil de détection de fuite mémoire afin de s'assurer que toute la mémoire que l'on alloue est bien désallouée. Le second objectif de cet exercice sera d'utiliser ce même outil afin de détecter de potentiel accès (lecture/écriture) dans des zones non allouées dans la mémoire. Cet outil se nomme valgrind. Nous allons l'utiliser pour détecter les fuites mémoires dans un programme ainsi que les accès interdits dans la mémoire. Le programme que l'on va étudier est le suivant :
#include <stdlib.h>
void f(void) {
int* x = malloc(10 * sizeof(int));
x[10] = 0;
}
int main(void) {
f();
return 0;
}
On va essayer de comprendre ce que valgrind peut nous dire d'un tel programme. Pour commencer on va compiler notre programme comme on compilerait un programme classique :
gcc program.c -o program
L'idée est ensuite d'utiliser valgrind pour obtenir les informations suivantes:
- Les fuites de mémoires (mémoire non désallouées) et leurs origines: pour cela on cherchera dans le manuel de valgrind l'option indiquant les appels à
malloc
non désallouées. Que dit valgrind concernant ces fuites ? - Les accès interdits. Il n'y a rien à faire de particulier pour ces cas, valgrind les donnes lui même. Comment sont-ils indiqués par valgrind ?
Maintenant que l'on a pu observé comment valgrind nous indique les erreurs présentent dans notre programme, nous allons voir comment l'on peut combiner l'utilisation de gdb
avec valgrind
pour traquer les erreurs dans notre programme.
L'objectif ici est d'utiliser gdb en parallèle de valgrind pour s'arrêter dès que valgrind détecte une erreur à l'exécution. Pour cela on va lancer notre programme avec valgrind avec l'option suivante :
valgrind --vgdb=yes --vgdb-error=0 ./program
Valgrind va alors ce lancer, mais notre programme sera mis en pause avant même de rentré dans la fonction main. Dans les quelques lignes que nous montre valgrind, on trouve une série d'instructions expliquant comment utiliser gdb avec valgrind pour que notre programme se lance.
- Cherchez dans la sortie de valgrind les commandes à rentré dans gdb pour pouvoir lancer notre programme. Lancez les.
En utilisant valgrind et gdb, corrigez le programme précédent pour qu'il n'y ait plus d'erreurs.