TP ‐ Environnement de Travail - vbridonneau/CoursSysteme GitHub Wiki

Environnement de Travail

Échauffement

Le manuel

Utiliser la commande man pour obtenir le manuel d'utilisation de

  1. 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.
  2. la commande man (oui le manuel a aussi un manuel d'utilisation).
  3. 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 ?

Commande top

Utilisez la commande top afin que celle-ci montre :

  1. Les programmes lancés par vous.
  2. Les performances de votre machine.

Exercices :

Débogage avec gdb

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.

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

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

  1. Nous allons commencer par la navigation:
    • Mettez un breakpoint à l'instruction précédant l'appel de la fonction init (breakpoint ou b).
    • 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 ou s).
    • Avancez de quelques instructions (next ou n).
    • Observez votre position dans le code (where)
    • Continuez l'exécution du programme (continue ou c).

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.

  1. 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 fonction init.
    • Avancez de quelques instructions afin d'être dans la boucle for.
    • Affichez le contenu de la variable i (print ou p).
    • Changez le contenu de cette variable à 9 (set variable i = 9).
    • Continuer l'exécution du programme. Que se passe-t-il ?

Makefile

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).
  1. 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 commande patsubst (ou sa cousine subst).

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

  1. É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.

Pour aller plus loin

Détection de fuite mémoire avec valgrind

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.

Valgrind x Gdb

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.

⚠️ **GitHub.com Fallback** ⚠️