C : Initation à GCC, GDB & Valgrind

Partager cet article

Temps estimé pour la lecture de cet article : 22 min

Salut les codeurs !

Cet court article est une introduction aux applications GCC, GDB et Valgrind. Ces logiciels vont nous permettre de débugger et optimiser nos programmes C. Pour ce faire on va se baser sur des exemples concrets. Let’s do this !

GCC – GNU Compiler Collection

GCC est un compilateur, c’est grâce à lui que l’on va pouvoir produire un binaire avec notre code source. L’intérêt de gcc, c’est qu’il offre un système d’options afin de permettre à l’utilisateur d’optimiser son code. Il suffit de préciser ces options lors de la compilation ainsi si notre code contient des erreurs, on va pouvoir facilement les identifier. Voici une liste des options que j’utilise le plus :

Option Description
-Werror Permet de transformer toutes les avertissements en erreurs.
-pedantic Affiche tous les avertissements demandaient par la norme ISO du C et du C++.
-Wall Affiche tous les problèmes de constructions courantes.
-Wextra Cette option permet de vérifier encore d’autres avertissements, on retrouve notamment le cas d’un pointeur comparé avec un entier dont la valeur est 0.
-std=standard Exemple : c99, (permet de déclarer une variable i dans une boucle).
-g Permet de pouvoir utiliser gdb sur le binaire compilé.
-O3 Optimisation du code pour avoir un gain de performance, pour plus d’infos : la doc.

Ce qui nous donne pour compiler un programme basique avec ces options :

gcc main.c -o main -W -Wall -pedantic -g -std=c99

GDB – GNU Project Debugger

On va maintenant voir comment utiliser le débugger GDB ! Prenons un cas concret, ici on a un programme très simple qui a pour but d’utiliser un pointeur. On a un entier x qui vaut 42 et on veut afficher cet entier en passant par un pointeur, seulement voila ici on s’y prends mal, en effet on essaye d’assigner au pointeur la valeur de l’entier et non son adresse mémoire. Du coup, on va obtenir un magnifique segmentation fault lors de l’exécution du binaire obtenu. GDB est là pour nous aider !

#include <stdio.h>
#include <stdlib.h>

int main(int argc, const char *argv[]) {
	int *pointer = NULL;
	int x = 42;
	
	pointer = x;
	printf("%d\n",*pointer);
	return EXIT_SUCCESS;
}

Pour pouvoir utiliser gdb, il faut préciser à la compilation l’argument -g :

gcc main.c -o main -g

Après, pour lancer gdb, il suffit de faire

gdb main

On obtient ensuite, un prompt de gdb, la commande r (ou run), permet donc de lancer le programme avec gdb. Le binaire se lance, on arrive sur notre erreur et on voit s’afficher ce magnifique segmentation fault mais maintenant gdb nous précise la ligne fautive et en effet elle se situe dans le printf(), on essaye d’afficher la valeur d’un pointeur mal initialisé… Du coup, grâce à gdb on a pu trouver notre erreur, ici sur cet exemple cela peut paraître dérisoire mais sur un projet de 4000 lignes, ça commence à devenir intéressant !

Exemples de commandes GDB

Beaucoup d’autres commandes peuvent être utile en voici une petite liste :

Commande Effet
break 25 place un point d’arrêt à la ligne 25
info breakpoints liste les points d’arrêts
delete efface les points d’arrêts
next exécute une instruction (ne rentre pas dans les fonctions)
step exécute une instruction (rentre potentiellement dans les fonctions)
finish exécute les instructions jusqu’à la sortie de la fonction
until 10 exécute les instructions jusqu’à la ligne 10
print var affiche la valeur de la variable
bt affiche l’état de la pile

Valgrind et exemple de fuite mémoire

Valgrind est une suite d’outils afin de débugger et suivre la mémoire utilisé par notre programme. Ainsi, il permet par exemple de vérifier si les allocations dynamiques qu’on a réalisé ont été libéré de la mémoire. Encore une fois, voyons ceci en pratique.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, const char *argv[]) {
	int **tab,i,j;

	tab = (int**) calloc(10,sizeof(int *));
	for (i = 0; i < 10; i++) {
		tab[i] = (int*)calloc(10,sizeof(int));
	}

	for (i = 0; i < 10; i++) {
		for (j = 0; j < 10; j++) {
			printf("%d ",tab[i][j]);
		}
		printf("\n");
	}

	return EXIT_SUCCESS;
}

On alloue un tableau à deux dimensions dont on ne libère pas la mémoire. Une fois de plus dans notre exemple cela saute aux yeux mais sur un gros projet, valgrind va nous permettre de localiser les allocations non libérés rapidement.

C - Valgrind

C – Valgrind

Debug du programme et explication de la gestion mémoire des pointeurs

On observe donc qu’on a effectué 11 allocations, ce qui est assez logique. En effet, on commence par allouer 80 octets, pour la première partie du tableau dynamique (10 * la taille d’un pointeur sur int soit 8 octets). Puis 400 octets pour les 100 cases du tableau (10*10*sizeof(int)). Pour ceux qui ont un peu de mal, avec les pointeurs de pointeurs voila comment on pourrait représenter notre tableau.

Tableau dynamique C

Tableau dynamique C

Du coup valgrind, nous explique ici qu’on ne libère rien et qu’on perd 480 octets. Dans notre cas pour éviter cela, il suffit de libérer la mémoire précédemment alloué, comme ceci :

for (i = 0; i < 10; i++) {
    free(tab[i]);
}
free(tab);

Voilou, en espérant que ça aurait aidé quelqu’un.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.