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