Le but de ce tutoriel est de vous apprendre à utiliser des outils de débogage des aspects mémoire (dynamiques, donc ; « run-time »). Mais avant tout, n'oubliez pas déjà d'utiliser les autres outils présentés pour le débogage : les options du compilateur, l'analyseur statique et gdb.

Dans ce tutoriel ci, nous allons présenter Address Sanitizer (alias « ASAN ») et Valgrind.

Mais pour cela, nous allons avoir besoin de bugs mémoire. Téléchargez ici un programme comprenant un florilège d'erreurs sur les pointeurs :

  1. utilisation de pointeur non initialisé ;
  2. retour d'adresse de variable locale ;
  3. utilisation de pointeur désalloué ;
  4. débordement « de tampon » ;
  5. désallocation de pointeur sur la pile (alloué statiquement) ;
  6. fuite de mémoire (pointeur non désalloué).

Commencez par regarder le programme fourni et comprendre son fonctionnement et ses erreurs (indiquées).

Les bonnes vielles méthodes

Avant d'utiliser de nouveaux outils, essayez de compiler puis d'analyser statiquement le code fourni.

Avec ces outils (options du compilateur et scan-build), vous devriez facilement trouver les erreurs 1 et 2 ci-dessus.
Laissez les pour le moment.

ASAN

Address Sanitizer (alias « ASAN ») est un outil d'analyse des défauts d'accès mémoire utilisant le compilateur. Pour l'utiliser, il faut ajouter l'option

-fsanitize=address

au compilateur.

Compilez avec cette option (ainsi que -g, en tout cas, et toutes les autres options que vous souhaitez), puis lancez le programme. Vous devriez obtenir quelque chose comme :

3-2i
0
-5+i
AddressSanitizer:DEADLYSIGNAL
=================================================================
==165699==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x55bf9afe15e8 bp 0x7ffd6a193c50 sp 0x7ffd6a193c30 T0)
==165699==The signal is caused by a READ memory access.
==165699==Hint: address points to the zero page.
    #0 0x55bf9afe15e7 in affiche complexe.c:76
    #1 0x55bf9afe13ab in main complexe.c:38
    #2 0x7f8874e2cbba in __libc_start_main ../csu/libc-start.c:308
    #3 0x55bf9afe1159 in _start (complexe+0x1159)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV complexe.c:76 in affiche
==165699==ABORTING

Cela vous dit qu'il y a un « Segmentation Fault » (SEGV) dans affiche() à la ligne 76, et que cette fonction a été appelée depuis la ligne 38 du main().
Voyez-vous de quoi il s'agit ?

Nous allons y revenir plus tard, mais voyons d'abord l'autre outil.

Valgrind

Valgrind est une suite d'outils d'analyse dynamique de code utilisant une machine virtuelle et la « compilation a la volée » (just-in-time (JIT) compilation).

Il s'utilise en lançant simplement valgrind devant le nom du programme à exécuter. Pour cela :

  1. supprimer l'exécutable précédemment compilé (car on ne va pas utiliser en même temps valgrind et ASAN !) :

     rm complexe
    
  2. recompilez mais SANS l'option -fsanitize=address (par contre gardez au moins l'option -g) ;

  3. lancez :

     valgrind ./complexe
    

Vous devriez obtenir quelque chose comme :

==165821== Memcheck, a memory error detector
==165821== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==165821== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==165821== Command: ./complexe
==165821== 
3-2i
==165821== Use of uninitialised value of size 8
==165821==    at 0x109336: affiche (complexe.c:76)
==165821==    by 0x1091BE: main (complexe.c:38)
==165821== 
==165821== Use of uninitialised value of size 8
==165821==    at 0x109358: affiche (complexe.c:76)
==165821==    by 0x1091BE: main (complexe.c:38)
==165821== 
[... plusieurs répétitions possibles en fonction de votre machine ...]
0  // ou une autre valeur
-5+i
==165821== Invalid read of size 8
==165821==    at 0x109336: affiche (complexe.c:76)
==165821==    by 0x109207: main (complexe.c:44)
==165821==  Address 0x0 is not stack'd, malloc'd or (recently) free'd
==165821== 
==165821== 
==165821== Process terminating with default action of signal 11 (SIGSEGV)
==165821==  Access not within mapped region at address 0x0
==165821==    at 0x109336: affiche (complexe.c:76)
==165821==    by 0x109207: main (complexe.c:44)
==165821==  If you believe this happened as a result of a stack
==165821==  overflow in your program's main thread (unlikely but
==165821==  possible), you can try to increase the size of the
==165821==  main thread stack using the --main-stacksize= flag.
==165821==  The main thread stack size used in this run was 8388608.
==165821== 
==165821== HEAP SUMMARY:
==165821==     in use at exit: 0 bytes in 0 blocks
==165821==   total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==165821== 
==165821== All heap blocks were freed -- no leaks are possible
==165821== 
==165821== Use --track-origins=yes to see where uninitialised values come from
==165821== For lists of detected and suppressed errors, rerun with: -s
==165821== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)
Erreur de segmentation

On y voit plus de choses entre l'affichage de a (3-2i) et les deux affichages suivants, et même aussi autre chose avant le crash final. De quoi s'agit-il ?

==165821== Use of uninitialised value of size 8
==165821==    at 0x109336: affiche (complexe.c:76)
==165821==    by 0x1091BE: main (complexe.c:38)

vous dit que dans l'appel à la fonction affiche() réalisé à la ligne 31 du main(), vous utilisez une valeur non initialisée.
Il vous le dit même au moins deux fois de suite. Pourquoi ?
Simplement parce que (1) le pointeur p_b n'est pas initialisé (première erreur) et (2) la valeur pointée (adresse quelconque) ne l'a pas non plus été (seconde erreur). Ensuite, en fonction de cette valeur non initialisée, plusieurs lignes de affiche() sont encore exécutées (ou pas), donnant autant de messages d'erreur.

Enfin le :

==165821== Invalid read of size 8
==165821==    at 0x109336: affiche (complexe.c:76)
==165821==    by 0x109207: main (complexe.c:44)
==165821==  Address 0x0 is not stack'd, malloc'd or (recently) free'd

juste avant le crash, vous dit justement que vous lisez 8 octets (size 8, soient 64 bits) invalides lors de l'appel affiche() à la ligne 44 du main(). C'est la même chose que ce que nous avions déjà vu avec les options du compilateur, l'analyse statique et aussi ASAN (c'est une erreur tellement grosse que tout le monde la voit ! ;-)).

Il est maintenant temps de corriger ce programme.

Exemple de correction

Je vous conseille de toujours commencer par corriger les erreurs détectées avec les outils les plus simples en premier.

Les options du compilateur

Normalement, si vous avez suivi les conseils des semaines précédentes, vous devriez compiler avec assez d'options pour trouver facilement l'erreur de retour d'adresse de variable locale.

Corrigez la (supprimez sans autre la fonction bad_addition() et son appel), puis recompilez. Normalement, cela devrait compiler sans warning (majeur, ceux qui ont -Wcast-qual, n'utilisez pas cette option ici).

L'analyseur statique

Utilisez l'analyseur statique (scan-build ; revoir si nécessaire les autres outils présentés pour le débogage) pour trouver une autre erreur. Corrigez la (p.ex. en supprimant la ligne 37).

Relancez l'analyseur statique. Il en trouve une autre !
Corrigez la également (p.ex. en déplaçant la ligne du free).

Relancez à nouveau l'analyseur statique. Il en trouve encore une autre !!
Corrigez la aussi (suppression de la ligne).

Relancez encore une fois l'analyseur statique. Il arrive encore à en trouver deux autres !!!!
Corrigez les aussi (suppression de la ligne 42 et ajout d'un free à la fin).

Relancez pour la dernière fois l'analyseur statique. Ca y est, ça passe !

Bilan à ce stade : 6 erreurs sur 7 trouvées.

ASAN

Compilez en ajoutant ASAN et lancez le programme.

Il trouve le buffer overflow :

==178554==ERROR: AddressSanitizer: heap-buffer-overflow on address [...]
WRITE of size 16 at [...]
    #0 0x556e52e43592 in main complexe.c:56
[...]

Laissez la pour le moment et voyons ce que dit valgrind.

valgrind

Supprimez l'exécutable et recompilez le sans ASAN ; puis lancez le avec valgrind. Il la trouve aussi :

==178672== Invalid write of size 8
==178672==    at 0x1092D3: main (complexe.c:56)
==178672==  Address 0x4a39540 is 0 bytes after a block of size 32 alloc'd
==178672==    at 0x4838B65: calloc (vg_replace_malloc.c:762)
==178672==    by 0x109290: main (complexe.c:53)
==178672== 
==178672== Invalid write of size 8
==178672==    at 0x1092D6: main (complexe.c:56)
==178672==  Address 0x4a39548 is 8 bytes after a block of size 32 alloc'd
==178672==    at 0x4838B65: calloc (vg_replace_malloc.c:762)
==178672==    by 0x109290: main (complexe.c:53)

C'est la même erreur que celle pointée par ASAN, mais valgrind la voit en 2 écritures de 8_octets, alors que ASAN la reportée comme une écriture de taille 16 octets. C'est une question de point de vue (les deux champs du Complexe, ou tout le Complexe lui-même).

Corrigez l'erreur et retestez avec ASAS et avec valgrind.

ASAN ou valgrind ?

C'est une question de goût. A vous de voir à l'usage.

Y a-t-il des erreurs que l'un voit et pas l'autre ? Personnellement, je n'en sais rien. Et j'utilise les deux pour être sûr ;-)

Ces outils peuvent aussi détecter les fuites de mémoire (que l'analyseur statique auraient ratées). Par exemple (supprimez les free que vous aviez ajouté) :

valgrind --leak-check=full ./complexe

(ASAN n'a a priori pas besoin d'option supplémentaire. Si ce n'est pas le cas sur votre machine, faites :

export ASAN_OPTIONS=detect_leaks=1

)