Skip to content

Instantly share code, notes, and snippets.

@GreenCandlePrinter
Last active October 6, 2015 18:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GreenCandlePrinter/efb650c69de706b367ba to your computer and use it in GitHub Desktop.
Save GreenCandlePrinter/efb650c69de706b367ba to your computer and use it in GitHub Desktop.
Initiation à l'utilisation des debuggers

I. LES PRINCIPAUX DEBUGGERS (GNU/LINUX)

I.1 VALGRIND (man valgrind)

Valgrind est une suite permettant le debugging / profiling de programmes. Elle est livrée avec plusieurs outils, tels que Memcheck, Helgrind ou encore Callgrind.
Nous parlerons uniquement de Memcheck, qui est l'outil activé par défaut, permettant de traquer les erreurs de gestion de la mémoire.
N'hésitez pas à chercher des informations à propos des autres utilitaires de la suite !

Usage

Afin d'analyser un processus avec valgrind, vous devrez le lancer grâce à une commande de cette forme (cf man valgrind) :
valgrind [options de valgrind] [chemin vers l'executable] [options de votre programme]

NB : la commande valgrind ./a.out est équivalente à valgrind --tool=memcheck ./a.out

Fonctionnement

Lors de l'execution d'un processus, si une erreur est détectée par Memcheck, une stack trace (ou stack backtrace) s'affichera sur votre terminal, qui correspond au cheminement d'appel de fonctions qui a mené à cette erreur.
Je vous recommande vivement de vous renseigner sur ce qu'est exactement une pile d'execution (call stack).
Memcheck pourra même vous indiquer avec précision la ligne qui a provoqué l'erreur, mais il faudra pour cela que vous utilisiez une option de gcc lors de la compilation. Il s'agit de -g qui permet de générer des informations de debug utilisables par valgrind (et gdb, dont nous parlerons plus tard). Il est aussi recommandé de ne pas activer les options d'optimisations de gcc (le man gcc explique pourquoi).

Les différents types d'erreurs

  • Accès à des zones non allouées (invalid read/write)
    Si vous tentez d'acceder en lecture ou en ecriture à une zone mémoire qui n'est pas allouée à votre executable, vous allez alors générer une erreur de type "invalid read/write of size N" (et potentiellement un crash), N étant la taille de cette zone. Un exemple commun est celui du parcours d'une chaine de caractères qui n'aurait pas de '\0' final.

  • Utilisation de valeurs non initialisées (uninitialised values)
    L'utilisation de variables non initialisées est une source d'erreurs courante. Pensez à bien initialiser chaque variable avant utilisation. Afin de detecter plus facilement les variables que vous auriez pu omettre, gcc fournit des options qui permettent d'augmenter la quantité et la diversité des warning qui seront lancés lors de la compilation. En voici une liste non exhaustive
    -W, -Wall, -Wextra
    Leur utilisation est fortement recommandée, car plus vous aurez d'erreurs lors de la compilation de votre programme, moins vous en aurez lors de l'execution.

  • Fuites mémoires (memory leaks)
    Chaque bloc de memoire alloué grace à malloc() doit être liberé grace à free() dès l'instant ou vous n'en aurez plus besoin. Dans le cas contraire votre programme finira par monopoliser beaucoup plus de ressources que necessaire, ce qui peut rapidement devenir gênant. Si vous ne me croyez pas, vous pouvez essayer d'executer ce code :

while (1)
    malloc(42); // kaboom

Puis ensuite cette version

while (1)
    free(malloc(42)); // free instantanement la memoire allouée
  • Free invalides (double frees, ...)
    Si vous choisissez une valeur arbitraire que vous assignez à un pointeur, que cette valeur ne corresponds pas à l'adresse du début d'un bloc alloué à votre programme, et que vous appellez free() avec ce pointeur en paramètre il y aura un invalid free. De la même façon, si vous essayez de liberer une adresse mémoire qui à déjà été libérée auparavant, vous allez obtenir une erreur du type "double free or corruption". Pour eviter un double free, il existe deux solutions : Soit vous avez fait une erreur, et il y a un free() en trop dans votre code, que vous devrez retrouver et supprimer. Soit, si vous ne pouvez pas eviter que le même pointeur soit utilisé deux fois par free(), il sera conseillé de le reinitialiser à NULL (ceci s'explique par le fait que free() n'a aucun effet sur un pointeur NULL, cf man free).

Exemple d'output commentée :

==PID== Memcheck, a memory error detector
==PID== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==PID== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==PID== Command: ./test
==PID==
==PID== Invalid free() / delete / delete[] / realloc()                                    [ TYPE D'ERREUR + STACK TRACE ]
==PID==    at 0x4C28ADC: free (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)  [ L'ERREUR VIENS DE CETTE FONCTION ]
==PID==    by 0x4005CD: main (test.c:19)                                                  [ QUI A ETE APPELLÉE DANS test.c l:19]
==PID==  Address 0x51e0040 is 0 bytes inside a block of size 42 free'd                    [ DETAILS A PROPOS L'ERREUR ]
==PID==    at 0x4C28ADC: free (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==PID==    by 0x4005C1: main (test.c:18)
==PID==
==PID==
==PID== HEAP SUMMARY:
==PID==     in use at exit: 110 bytes in 11 blocks                     [ MEMOIRE TOUJOURS ALLOUÉE A LA SORTIE DU PROCESSUS ]
==PID==   total heap usage: 12 allocs, 1 frees, 126 bytes allocated    [ NOMBRE D'ALLOCATIONS / LIBERATIONS EFFECTUÉES ]
==PID==
==PID== LEAK SUMMARY:                                 [ INFORMATIONS SUR LES FUITES MEMOIRE ]
==PID==    definitely lost: 110 bytes in 11 blocks    [ FUITES MEMOIRE ]
==PID==    indirectly lost: 0 bytes in 0 blocks       [ FUITES INDIRECTES (EX: OUBLI DE FREE LE MEMBRE D'UNE STRUCTURE) ]
==PID==      possibly lost: 0 bytes in 0 blocks       [ ... ]
==PID==    still reachable: 0 bytes in 0 blocks       [ ... ]
==PID==         suppressed: 0 bytes in 0 blocks
==PID== Rerun with --leak-check=full to see details of leaked memory
==PID==
==PID== For counts of detected and suppressed errors, rerun with: -v
==PID== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 2 from 2)

NB : memcheck ralenti considerablement l'execution du processus

Quelques options utiles :

--leak-check=no/summary/full (analyse detaillée des leaks)
--trace-malloc=no/yes (affiche en live les appels a malloc/free)
--trace-children=no/yes (monitoring des enfants du process)
--trace-fds=no/yes (monitoring des fds)
--track-origins=no/yes (donne des informations sur l'origine des valeurs non initialisées)

I.2 GDB (man gdb)

Il est parfois nécessaire d'avoir une analyse plus approfondie que celle permise par Valgrind. Nous allons maintenant parler de GDB, qui permet de comprendre en temps réel les moindres détails du fonctionnement de votre code.

Usage et fonctionnement

Pour commencer à utiliser GDB il suffit de faire : gdb chemin_vers_votre_executable.
Vous pouvez aussi vous attacher à un processus en cours d'exécution grâce à : gdb -p pid.

NB : avoir un binaire compilé avec -g est la encore un gros plus

GDB s'attache à un processus, et va permettre d'effectuer plusieurs types d'actions. Il fonctionne comme un shell, c'est à dire qu'il s'utilise avec des lignes de commandes.

Les principales commandes

Démarrer l'exécution du programme :

  • run (ou r) :
    usage : r path_to_executable [args]
    exemple : r ./a.out --arg1 "argument 2" "argument 3"

Gestion des points d'arrêts (breakpoints, watchpoints) :

Un point d'arrêt est un endroit précis ou l'on va demander au debuggeur de mettre en pause l'exécution d'un processus. Cet état de pause va permettre d'analyser en profondeur l'état de l'execution à un instant T.

  • break (ou b) :
    usage : b [fichier:]nom_de_fonction/numero de ligne
    Place un point d'arrêt à une ligne, ou une fonction précise. Vous pouvez de manière optionnelle préciser le fichier dans lequel se situe la ligne / fonction souhaitée.

  • watch :
    usage : watch nom_de_variable
    Place un watchpoint sur une variable. L'execution du processus sera interrompue des lors que sa valeur sera modifiée

  • info breakpoints / watchpoints :
    usage : i b ou i watchpoint
    Affiche une liste des breakpoints / watchpoints

  • delete (ou d) :
    usage : d [n]
    Supprime tous les breakpoints.
    Si n est précisé, seul le breakpoint numéro n sera affecté

Maitriser l'exécution :

Une fois l'exécution mise en pause par l'intermediaire des breakpoints, plusieurs possibilités s'offrent à nous pour analyser le comportement du processus.

  • disp :
    usage : disp expr
    Affiche la valeur de expr au cours de l'exécution
    expr peut être n'importe quelle expression valide (nom de variable, appel de fonction, ...)

  • step (ou s) :
    Poursuit l'exécution jusqu'à la ligne suivante

  • next (ou n) :
    Poursuit l'exécution jusqu'à la ligne suivante
    Contrairement à step, next ne rentre pas dans les appels de fonctions

  • cont (ou c) :
    Reprends l'exécution normale du processus

  • i args :
    Affiche la liste des arguments de la fonction actuelle et leur valeurs

  • i reg :
    Affiche la liste des registres du processus et leurs contenu

Manipulation de la call stack :

  • bt :
    Affiche la stack backtrace

  • up / down :
    Deplace gdb un niveau plus haut ou plus bas dans la call stack

Il existe beaucoup d'autres commandes, je pense que vous avez compris que google est votre ami !


I.3 STRACE (man strace)

Strace est un petit programme qui va vous permettre de voir rapidement et facilement les interactions entre un processus et votre système.

Usage et fonctionnement

Pour lancer strace il suffit de faire : strace chemin_vers_votre_executable
De la même manière que gdb, vous pouvez l'attacher à un processus en cours d'exécution de cette manière : strace -p pid

strace permet de tracer en temps réel tous les appels systèmes effectués par un processus (y compris les valeurs de leur paramètres et les valeurs de retour) ainsi que les signaux reçus et émis.

Exemple d'output commentée

ça arrive bientôt

II. Un peu de pratique

Puisque je sais que vous n'aimez pas lire, j'ai conçu un petit code pour que vous puissiez vous entrainer à utiliser tous ces super outils !

Les règles du JEU

  • Vous trouverez ci-après un code source peu complexe, comportant plusieurs bugs. Votre objectif est de les trouver à l'aide des outils que je vous ai presenté plus tôt.

  • Le programme une fois débuggé génère simplement une liste de chaines de caractères aléatoires.

  • Le programme s'utilise de cette façon : ./a.out size count, ou size représente la taille d'une chaine générée, et count le nombre de chaines.

  • Le code final doit être assez ressemblant au code de base, si de trop lourdes modifications ont été apportées, c'est une erreur de votre part.

Le code

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

#ifndef BASE
# define BASE   10
#endif

void    str_randomize(char *s, long size)
{
  int   i;

  for (i = 0; i <= size; i++)
    s[i] = (rand() + 32) % 127;
}

char    **str_array_alloc(long count)
{
  return (malloc(count * sizeof(char *)));
}

void    str_alloc(char *s, long size)
{
  s = malloc(sizeof(char) * size);
}

void    str_array_print(char **result)
{
  int   i;

  while (result[i])
    {
      printf("%s\n", result[i]);
      i++;
    }
}

void    init_generator(char *av[], long str_size, long str_count)
{
  srand(time(NULL));
  str_size = strtol(av[1], NULL, BASE);
  str_count = strtol(av[2], NULL, BASE);
}

int     array_generate(char ***result, long size, long count)
{
  int   i;

  *result = str_array_alloc(count);
  if (*result = NULL)
    {
      perror("allocation failed ");
      return (1);
    }
  for (i = 0; *result[i] != NULL; i++)
    {
      str_alloc(**result, size);
      str_randomize(**result, size);
    }
  return (0);
}

int     main(int argc, char *argv[])
{
  char  **result;
  long  str_size = 0;
  long  str_count = 0;

  init_generator(argv, str_size, str_count);
  if (array_generate(&result, str_size, str_count) == 1)
    return (1);
  str_array_print(result);
  free(result);
  return (0);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment