[Linux] - Tutorial Valgrind

[Linux] - Tutorial Valgrind

Postby morpheus » 01 May 2010, 23:58

Tutorial Valgrind



1. Introducere


Cui i se adreseaza acest tutorial ?
Oricui isi propune sa scrie aplicatii C/C++ ruland pe sisteme Linux.

Ce este Valgrind ?
Valgrind este o suita de programe utilitare, putand fi folosit pentru depanarea problemelor privind utilizarea memoriei, detectarea memory leak-urilor si pentru profiling.

Cum functioneaza ?
Valgrind este, in esenta, o masina virtuala. Atunci cand executam un program folosind Valgrind, acesta este rulat in respectiva masina virtuala. Cu aceasta ocazie, sunt aplicate anumite tehnici si procedee de instrumentare a codului care permit detectia unor categorii de erori de programare.
Valgrind ruleaza doar pe platforme Linux.
Exista programe similare si pentru utilizatorii Windows, dar, de obicei, sunt comerciale. Dintre alternativele valabile pe sisteme Windows, putem enumera: IBM Purify, Parasoft Insure++, etc.

Care sunt categoriile de erori pe care le poate detecta Valgrind ?
Valgrind consta dintr-o suita de programe utilitare.
Proababil cel mai utilizat dintre ele este memcheck, destinat analizei utilizarii memoriei.

Acesta poate detecta erori precum:
- Utilizarea memoriei neinitializate
- Accesarea memoriei dezalocate
- Buffer overflows, pentru buffer-ele alocate pe heap
- Anumite cazuri de accesare incorecta a memoriei pe stiva curenta
- Memory leaks
- Utilizarea incorecta a functiilor de alocare/dezalocare a memoriei

In afara de memcheck, mai exista:
- Callgrind -> util pentru generarea de callgraph-uri (pentru profiling)
- Cachegrind -> util pentru profiling privind utilizarea cache-urilor
- Massif -> util pentru profiling privind utilizarea memoriei alocate dinamic
- Helgrind -> detecteaza greseli tipice in utilizarea thread-urilor POSIX

Cum il instalam ?
Este disponibil aici: http://valgrind.org/
Pe sistemele Debian (sau derivate din Debian, precum Ubuntu), se poate instala executand:
  1.  
  2. sudo apt-get install valgrind
  3.  


2. Utilizare



2.1. Detectia utilizarii memoriei neinitializate

Avem urmatorul cod (uninit_memory.c):
  1.  
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4.  
  5. int main()
  6. {
  7.     int i;
  8.     /* bug -> uninitialized variable used */
  9.     if (i == 0)
  10.         printf("Is zero\n");
  11.     else
  12.         printf("Non zero\n");
  13.        
  14.     return EXIT_SUCCESS;
  15. }
  16.  


Il compilam:
  1.  
  2. gcc -Wall -Wextra -g -O0 uninit_memory.c -o uninit_memory
  3.  


Ruland folosind utilitarul memcheck:
  1.  
  2. valgrind --tool=memcheck ./uninit_memory
  3.  

obtinem:
  1.  
  2. ==8365== Conditional jump or move depends on uninitialised value(s)
  3. ==8365==    at 0x80483D9: main (uninit_memory.c:8)
  4.  

Precum se poate observa, Valgrind raporteaza o eroare la linia 8 (if (i == 0)), privind utilizarea unei variabile neinitializate.

Detectia functioneaza si in cazul unui array alocat pe stiva (uninit_array.c).

  1.  
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. #include <string.h>
  5.  
  6. int main()
  7. {
  8.     char name[256];
  9.     /* bug -> uninitialized array used */
  10.     if (!strcmp(name, "root"))
  11.         printf("Hello superuser\n");
  12.     else
  13.         printf("Hello user\n");
  14.        
  15.     return EXIT_SUCCESS;
  16. }
  17.  

Compilare:
  1.  
  2. gcc -Wall -Wextra -g -O0 uninit_array.c -o uninit_array
  3.  

Executand folosind memcheck:
  1.  
  2. valgrind --tool=memcheck ./uninit_array
  3.  

obtinem:
  1.  
  2. ==9104== Conditional jump or move depends on uninitialised value(s)
  3. ==9104==    at 0x40279F3: strcmp (mc_replace_strmem.c:337)
  4. ==9104==    by 0x8048488: main (uninit_array.c:9)
  5.  

Valgrind raporteaza o eroare la linia if (strcmp(name, "root")), pentru ca array-ul name se foloseste neinitializat.

Sa vedem daca Valgrind detecteaza acest tip de eroare si in cazul utilizarii unui array alocat dinamic (pe heap).
Avem urmatorul cod sursa (uninit_dynamic_array.c):
  1.  
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. #include <string.h>
  5.  
  6. int main()
  7. {
  8.     char *name = (char *)malloc(256 * sizeof(char));
  9.     /* bug -> uninitialized array used */
  10.     if (!strcmp(name, "root"))
  11.         printf("Hello superuser\n");
  12.     else
  13.         printf("Hello user\n");
  14.        
  15.     free(name);
  16.        
  17.     return EXIT_SUCCESS;
  18. }
  19.  

Compilare:
  1.  
  2. gcc -Wall -Wextra -g -O0 uninit_dynamic_array.c -o uninit_dynamic_array
  3.  

Executand folosind memcheck:
  1.  
  2. valgrind --tool=memcheck --leak-check=yes ./uninit_dynamic_array
  3.  

obtinem
  1.  
  2. ==9385== Conditional jump or move depends on uninitialised value(s)
  3. ==9385==    at 0x40279F3: strcmp (mc_replace_strmem.c:337)
  4. ==9385==    by 0x8048496: main (uninit_dynamic_array.c:9)
  5.  

Valgrind continua sa detecteze problema.
Precum se poate observa, fata de situatiile anterioare, am utilizat in plus optiunea --leak-check=yes, pentru a detecta posibilele erori de tip memory leak (pentru ca acum alocam dinamic memorie). In cazul programului de mai sus, nu avem astfel de erori.

2.2. Detectia acceselor la memoria dezalocata

Avem urmatorul cod sursa (deallocated_memory_access.c):
  1.  
  2. #include <stdlib.h>
  3.  
  4. int main()
  5. {
  6.     int* p = (int*) malloc(sizeof(int));
  7.     *p = 1;
  8.     free(p);
  9.     /* bug - access to deallocated memory pointed by p */
  10.     *p = 2;
  11.     return EXIT_SUCCESS;
  12. }
  13.  

In acest program, se acceseaza memorie dezalocata la urmatoarea linie: *p = 2.

Compilare:
  1.  
  2. gcc -Wall -Wextra -g -O0 deallocated_memory_access.c -o deallocated_memory_access
  3.  

Executand folosind memcheck:
  1.  
  2. valgrind --tool=memcheck --leak-check=yes ./deallocated_memory_access
  3.  

obtinem:
  1.  
  2. ==11741== Invalid write of size 4
  3. ==11741==    at 0x804842B: main (deallocated_memory_access.c:9)
  4. ==11741==  Address 0x41b1028 is 0 bytes inside a block of size 4 free'd
  5. ==11741==    at 0x4025DFA: free (vg_replace_malloc.c:323)
  6. ==11741==    by 0x8048427: main (deallocated_memory_access.c:7)
  7.  


Mesajul oferit de catre Valgrind ne spune urmatoarele lucruri:

- s-a facut un acces invalid la memorie, in mod scriere, pe 4 octeti (dimensiunea tipului int, pe sistemul meu)
- accesul s-a facut la linia 9 in fisierul deallocated_memory_access.c
- adresa la care s-a incercat scrierea a fost 0x41b1028. Aceasta adresa este localizata intr-un bloc de memorie alocat dinamic, de dimensiune 4 octeti (iarasi egal cu dimensiunea int-ului pe sistemul meu), dar care a fost in prealabil dezalocat, la linia 7, in fisierul deallocated_memory_access.c

Utilizatorii limbajului C++ se pot intreba daca detectia functioneaza si in cazul in care memoria este alocata folosind operatorul new si dezalocata folosind operatorul delete.
Avem urmatorul cod C++ (deallocated_memory_access.cpp)
  1.  
  2. #include <cstdlib>
  3.  
  4. int main()
  5. {
  6.     int* p = new int;
  7.     *p = 1;
  8.     delete p;
  9.     /* bug - access to deallocated memory pointed by p */
  10.     *p = 2;
  11.     return EXIT_SUCCESS;
  12. }
  13.  

Compilare:
  1.  
  2. g++ -Wall -Wextra -g -O0 deallocated_memory_access.cpp -o deallocated_memory_access
  3.  

Executand folosind memcheck:
  1.  
  2. valgrind --tool=memcheck --leak-check=yes ./deallocated_memory_access
  3.  

obtinem:
  1.  
  2. ==12503== Invalid write of size 4
  3. ==12503==    at 0x804852B: main (deallocated_memory_access.cpp:9)
  4. ==12503==  Address 0x42d6028 is 0 bytes inside a block of size 4 free'd
  5. ==12503==    at 0x402599A: operator delete(void*) (vg_replace_malloc.c:342)
  6. ==12503==    by 0x8048527: main (deallocated_memory_access.cpp:7)
  7.  

Precum se poate observa, detectia a fost efectuata si in acest caz.

In continuare, dorim sa vedem daca detectia functioneaza si in cazul unui array, alocat folosind operatorul new[], dezalocat folosind operatorul delete [], apoi accesat (deallocated_array_access.cpp)
  1.  
  2. #include <iostream>
  3.  
  4. int main()
  5. {
  6.     int * array = new int[10];
  7.     for (int i = 0; i < 10; ++i)
  8.         array[i] = i;
  9.    
  10.     delete [] array;
  11.  
  12.     for (int i = 0; i < 10; ++i)
  13.         std::cout << array[i] << "\n";
  14. }
  15.  

Compilare:
  1.  
  2. g++ -Wall -Wextra -g -O0 deallocated_array_access.cpp -o deallocated_array_access
  3.  

Executand folosind memcheck:
  1.  
  2. valgrind --tool=memcheck --leak-check=yes ./deallocated_array_access
  3.  

obtinem:
  1.  
  2. ==12842== Invalid read of size 4
  3. ==12842==    at 0x8048766: main (deallocated_array_access.cpp:12)
  4. ==12842==  Address 0x42d6028 is 0 bytes inside a block of size 40 free'd
  5. ==12842==    at 0x402545A: operator delete[](void*) (vg_replace_malloc.c:364)
  6. ==12842==    by 0x8048753: main (deallocated_array_access.cpp:9)
  7.  

Acest mesaj ne spune urmatoarele lucruri:

- se face un acces invalid la memorie, in mod citire (read), pe 4 octeti
- accesul se face la linia 12, in fisierul deallocated_array_access.cpp (adica la linia std::cout << array[i] << "\n";)
- adresa accesata in mod incorect este 0x42d6028, aflandu-se intr-un block de memorie avand dimensiunea de 40 de octeti, alocat dinamic dar dezalocat in prealabil
- blocul de memorie a fost dezalocat la linia 9, in fisierul deallocated_array_access.cpp (adica delete [] array;)

2.3. Detectia problemelor de tip "memory leak"

Pentru cei nefamiliarizati cu termenul, aceast gen de probleme se refera la situatiile in care un program aloca dinamic memorie, dar nu o mai dezaloca.
Valgrind este un utilitar foarte bun in gasirea acestor tipuri de probleme. Exista si alte utilitare ce pot fi folosite, pe sisteme, Linux, pentru detectia acestor situatii (de exemplu mpatrol).

Urmatorul cod (memory_leaks.cpp) expune acest tip de problema.
  1.  
  2. #include <iostream>
  3. #include <cstdlib>
  4.  
  5. int main()
  6. {
  7.     int i;
  8.    
  9.     int *c_p = (int *)std::malloc(sizeof(int));
  10.     *c_p = 1;
  11.     int *c_q = (int *)std::malloc(10 * sizeof(int));
  12.     for (i = 0; i < 10; ++i)
  13.         c_q[i] = i;
  14.    
  15.     for (i = 0; i < 10; ++i)
  16.         std::cout << c_q[i] << "\n";
  17.        
  18.    
  19.     int *cpp_p = new int;
  20.     *cpp_p = 1;
  21.     int *cpp_q = new int[10];
  22.     for (i = 0; i < 10; ++i)
  23.         cpp_q[i] = i;
  24.  
  25.     for (i = 0; i < 10; ++i)
  26.         std::cout << cpp_q[i] << "\n";
  27.        
  28.     return EXIT_SUCCESS;
  29. }
  30.  

Compilare:
  1.  
  2. g++ -Wall -Wextra -g -O0 memory_leaks.cpp -o memory_leaks
  3.  

Executand folosind memcheck:
  1.  
  2. valgrind --tool=memcheck --leak-check=yes ./memory_leaks
  3.  

obtinem:
  1.  
  2. ==13843== 4 bytes in 1 blocks are definitely lost in loss record 1 of 4
  3. ==13843==    at 0x40269EE: operator new(unsigned int) (vg_replace_malloc.c:224)
  4. ==13843==    by 0x80487E4: main (memory_leaks.cpp:18)
  5. ==13843==
  6. ==13843==
  7. ==13843== 4 bytes in 1 blocks are definitely lost in loss record 2 of 4
  8. ==13843==    at 0x4026FDE: malloc (vg_replace_malloc.c:207)
  9. ==13843==    by 0x804875C: main (memory_leaks.cpp:8)
  10. ==13843==
  11. ==13843==
  12. ==13843== 40 bytes in 1 blocks are definitely lost in loss record 3 of 4
  13. ==13843==    at 0x402630E: operator new[](unsigned int) (vg_replace_malloc.c:268)
  14. ==13843==    by 0x80487FC: main (memory_leaks.cpp:20)
  15. ==13843==
  16. ==13843==
  17. ==13843== 40 bytes in 1 blocks are definitely lost in loss record 4 of 4
  18. ==13843==    at 0x4026FDE: malloc (vg_replace_malloc.c:207)
  19. ==13843==    by 0x8048774: main (memory_leaks.cpp:10)
  20. ==13843==
  21. ==13843== LEAK SUMMARY:
  22. ==13843==    definitely lost: 88 bytes in 4 blocks.
  23. ==13843==      possibly lost: 0 bytes in 0 blocks.
  24. ==13843==    still reachable: 0 bytes in 0 blocks.
  25. ==13843==         suppressed: 0 bytes in 0 blocks.
  26.  


Observam faptul ca Valgrind raporteaza locatiile in care s-au facut alocari de memorie care au dus la memory leaks (pentru care nu au existat dezalocari):

-> Linia 8: int *c_p = (int *)std::malloc(sizeof(int));
S-au alocat 4 octeti, folosind functia malloc, si nu au mai fost dezalocati

-> Linia 10: int *c_q = (int *)std::malloc(10 * sizeof(int));
S-au alocat 10 x 4 = 40 de octeti, folosind functia malloc, si nu au mai fost dezalocati

->Linia 18: int *cpp_p = new int;
S-au alocat 4 octeti, folosind operatorul new, si nu au mai fost dezalocati

->Linia 20: int *cpp_q = new int[10];
S-au alocat 10 x 4 = 40 de octeti, folosind operatorul new[], si nu au mai fost dezalocati

Avem un memory leak de: 4 + 40 + 4 + 40 = 88 de octeti, lucru specificat si in sectiunea LEAK SUMMARY a raportului oferit de catre Valgrind

Haideti sa rezolvam problemele raportate de catre Valgrind, sa rulam din nou utilitarul si sa vedem cum arata noul raport.
Noul fisier sursa este no_memory_leaks.cpp:
  1.  
  2. #include <iostream>
  3. #include <cstdlib>
  4.  
  5. int main()
  6. {
  7.     int i;
  8.    
  9.     int *c_p = (int *)std::malloc(sizeof(int));
  10.     *c_p = 1;
  11.     int *c_q = (int *)std::malloc(10 * sizeof(int));
  12.     for (i = 0; i < 10; ++i)
  13.         c_q[i] = i;
  14.    
  15.     for (i = 0; i < 10; ++i)
  16.         std::cout << c_q[i] << "\n";
  17.        
  18.     std::free(c_p);
  19.     std::free(c_q)
  20.    
  21.     int *cpp_p = new int;
  22.     *cpp_p = 1;
  23.     int *cpp_q = new int[10];
  24.     for (i = 0; i < 10; ++i)
  25.         cpp_q[i] = i;
  26.  
  27.     for (i = 0; i < 10; ++i)
  28.         std::cout << cpp_q[i] << "\n";
  29.        
  30.     delete cpp_p;
  31.     delete [] cpp_q;
  32.        
  33.     return EXIT_SUCCESS;
  34. }
  35.  

Compilare:
  1.  
  2. g++ -Wall -Wextra -g -O0 no_memory_leaks.cpp -o no_memory_leak
  3.  

Executand folosind memcheck:
  1.  
  2. valgrind --tool=memcheck --leak-check=yes ./no_memory_leaks
  3.  

obtinem:
  1.  
  2. ==14663== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 19 from 1)
  3. ==14663== malloc/free: in use at exit: 0 bytes in 0 blocks.
  4. ==14663== malloc/free: 4 allocs, 4 frees, 88 bytes allocated.
  5. ==14663== For counts of detected errors, rerun with: -v
  6. ==14663== All heap blocks were freed -- no leaks are possible.
  7.  

Precum se poate observa, Valgrind nu mai raporteaza nici o problema de tip memory leak.

2.4. Utilizarea incorecta a functiilor de alocare/dezalocare a memoriei

O problema destul de frecvent intalnita de catre programatorii C++ incepatori este utilizarea operatorului delete in locul operatorului delete [], pentru a dezaloca un array alocat dinamic.
Haideti sa vedem daca Valgrind detecteaza aceasta situatie (array_delete_missuse.cpp):
  1.  
  2. #include <iostream>
  3. #include <cstdlib>
  4.  
  5. int main()
  6. {
  7.     int i;
  8.     int *p = new int[10];
  9.     for (i = 0; i < 10; ++i)
  10.         p[i] = i;
  11.    
  12.     for (i = 0; i < 10; ++i)
  13.         std::cout << p[i] << "\n";
  14.  
  15.     /* bug - should be delete [] p */
  16.     delete p;
  17.    
  18.     return EXIT_SUCCESS;
  19. }
  20.  

Compilare:
  1.  
  2. g++ -Wall -Wextra -g -O0 array_delete_missuse.cpp -o array_delete_missuse
  3.  

Executand folosind memcheck:
  1.  
  2. valgrind --tool=memcheck --leak-check=yes ./array_delete_missuse
  3.  

obtinem:
  1.  
  2. ==15289== Mismatched free() / delete / delete []
  3. ==15289==    at 0x402599A: operator delete(void*) (vg_replace_malloc.c:342)
  4. ==15289==    by 0x804878B: main (array_delete_missuse.cpp:15)
  5. ==15289==  Address 0x42d6028 is 0 bytes inside a block of size 40 alloc'd
  6. ==15289==    at 0x402630E: operator new[](unsigned int) (vg_replace_malloc.c:268)
  7. ==15289==    by 0x804871C: main (array_delete_missuse.cpp:7)
  8.  

Precum se poate observa, Valgrind raporteaza faptul ca s-a incercat eliberarea memoriei intr-un mod incorect, la linia 15 (delete p;), in fisierul array_delete_missuse.cpp

In general, regulile care trebuiesc urmate in privinta dezalocarii memoriei, in C++, sunt urmatoarele:

- memoria alocata folosind functiile malloc()/calloc()/realloc() trebuie sa fie dezalocata folosind functia free()
- memoria alocata folosind operatorul new trebuie dezalocata folosind operatorul delete
- memoria alocata folosind operatorul new [] trebuie dezalocata folosind operatorul delete []

Valgrind va detecta situatiile in care aceasta regula este incalcata (de exemplu in cazul in care se aloca memorie folosind functia malloc(), dar se dezaloca folosind operatorul delete).

O alta posibila problema este incercarea de a dezaloca o zona de memorie care nu a fost alocata dinamic.
Exemplul urmator (invalid_dealloc.c) demonstreaza acest tip de problema.
  1.  
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4.  
  5. #define NUMBER_ELEMS 10
  6.  
  7. int main()
  8. {
  9.     int i;
  10.     int array[NUMBER_ELEMS];
  11.  
  12.     for (i = 0; i < NUMBER_ELEMS; i++)
  13.     {
  14.         array[i] = i;
  15.         printf("array[%d] = %d\n", i, array[i]);
  16.     }
  17.  
  18.     free(array);
  19.  
  20.     return EXIT_SUCCESS;
  21. }
  22.  

Compilare:
  1.  
  2. gcc -Wall -Wextra -g -O0 invalid_dealloc.c -o invalid_dealloc
  3.  

Executand folosind memcheck:
  1.  
  2. valgrind --tool=memcheck --leak-check=yes ./invalid_dealloc
  3.  

obtinem:
  1.  
  2. ==19852== Invalid free() / delete / delete[]
  3. ==19852==    at 0x4025DFA: free (vg_replace_malloc.c:323)
  4. ==19852==    by 0x804844A: main (invalid_dealloc.c:17)
  5. ==19852==  Address 0xbeaacd28 is on thread 1's stack
  6.  

Mesajul ne spune faptul ca, la linia 17 in fisierul invalid_dealloc.c, am incercat sa dezalocam o zona de memorie alocata pe stiva.

De asemenea, Valgrind este capabil sa detecteze probleme de tip double-free (in care o zona de memorie alocata dinamic este dezalocata de doua sau mai multe ori).
Exemplul urmator demonstreaza acest lucru (double_free_error.c)
  1.  
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4.  
  5. int main()
  6. {
  7.     int* p = (int*)malloc(sizeof(int));
  8.     *p = 5;
  9.     printf("%d\n", *p);
  10.     free(p);
  11.     free(p);
  12.    
  13.     return EXIT_SUCCESS;
  14. }
  15.  

Compilare:
  1.  
  2. gcc -Wall -Wextra -g -O0 double_free_error.c -o double_free_error
  3.  

Executand folosind memcheck:
  1.  
  2. valgrind --tool=memcheck --leak-check=yes ./double_free_error
  3.  

obtinem:
  1.  
  2. ==20168== Invalid free() / delete / delete[]
  3. ==20168==    at 0x4025DFA: free (vg_replace_malloc.c:323)
  4. ==20168==    by 0x8048477: main (double_free_error.c:10)
  5. ==20168==  Address 0x41b1028 is 0 bytes inside a block of size 4 free'd
  6. ==20168==    at 0x4025DFA: free (vg_replace_malloc.c:323)
  7. ==20168==    by 0x804846C: main (double_free_error.c:9)
  8.  

Analizand mesajul, se observa ca a avut loc o operatie invalida de dezalocare, utilizand functia free(), la linia 10 in fisierul double_free_error.c.

2.5. Detectia problemelor de tip buffer-overflow

Problemele de tip buffer-overflow trebuiesc tratate in modul cel mai serios. Ele preprezinta un risc de securitate major, fiind o tinta predilecta a exploit-urilor, putandu-se ajunge chiar la executia de cod arbitrar pe sistemul "victimei", cu drepturi de super-user.
Incepem cu un exemplu simplu de buffer overflow, in cazul unui buffer alocat dinamic (pe heap) (simple_buffer_overflow.c)
  1.  
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4.  
  5. #define NUMBER_ELEMS 10
  6.  
  7. int main()
  8. {
  9.     int i;
  10.     int *array = malloc(NUMBER_ELEMS * sizeof(int));
  11.    
  12.     for (i = 1; i <= NUMBER_ELEMS; i++)
  13.     {
  14.         array[i] = i;
  15.         printf("array[%d] = %d\n", i, array[i]);
  16.     }
  17.    
  18.     free(array);
  19.    
  20.     return EXIT_SUCCESS;
  21. }
  22.  

Compilare:
  1.  
  2. gcc -Wall -Wextra -g -O0 simple_buffer_overflow.c -o simple_buffer_overflow
  3.  


Daca rulez executabilul (fara sa folosesc Valgrind), obtin urmatorul output pe sistemul meu.
  1.  
  2. ./simple_buffer_overflow
  3. array[1] = 1
  4. array[2] = 2
  5. array[3] = 3
  6. array[4] = 4
  7. array[5] = 5
  8. array[6] = 6
  9. array[7] = 7
  10. array[8] = 8
  11. array[9] = 9
  12. array[10] = 10
  13.  


Aparent nu e nici o problema, programul nu a "crapat". Dar nu e deloc asa ... programul prezinta un buffer-overflow evident la linia array[i] = i, pentru cazul in care i = NUMBER_ELEMS. Iar daca nu a "crapat" in cazul meu, nu inseamna ca atunci cand va fi rulat din nou (eventual pe un alt sistem) nu se va intampla.
Erorile de acest gen duc la coruperea memoriei, care cauzeaza de obicei un comportament impredictibil al programului.

Sa rulam programul utilizand Valgrind:
  1.  
  2. valgrind --tool=memcheck --leak-check=yes ./simple_buffer_overflow
  3.  

Se obtine:
  1.  
  2. ==17295== Invalid write of size 4
  3. ==17295==    at 0x804845B: main (simple_buffer_overflow.c:13)
  4. ==17295==  Address 0x41b1050 is 0 bytes after a block of size 40 alloc'd
  5. ==17295==    at 0x4026FDE: malloc (vg_replace_malloc.c:207)
  6. ==17295==    by 0x8048440: main (simple_buffer_overflow.c:9)
  7. ==17295==
  8. ==17295== Invalid read of size 4
  9. ==17295==    at 0x8048466: main (simple_buffer_overflow.c:14)
  10. ==17295==  Address 0x41b1050 is 0 bytes after a block of size 40 alloc'd
  11. ==17295==    at 0x4026FDE: malloc (vg_replace_malloc.c:207)
  12. ==17295==    by 0x8048440: main (simple_buffer_overflow.c:9)
  13. array[10] = 10
  14.  

In acest moment devine evident faptul ca programul acceseaza incorect memoria (in afara buffer-ului):
- in mod scriere (la linia 13): array[i] = i;
- in mod citire (la linia 14): printf("array[%d] = %d\n", i, array[i]);
in cazul in care variabila i are valoarea 10.

Sa facem o mica modificare in codul anterior. Buffer-ul nu va mai fi alocat dinamic, ci va fi alocat pe stiva.
Rezulta urmatorul cod (stack_buffer_overflow.c):
  1.  
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4.  
  5. #define NUMBER_ELEMS 10
  6.  
  7. int main()
  8. {
  9.     int i;
  10.     int array[NUMBER_ELEMS];
  11.    
  12.     for (i = 1; i <= NUMBER_ELEMS; i++)
  13.     {
  14.         array[i] = i;
  15.         printf("array[%d] = %d\n", i, array[i]);
  16.     }
  17.  
  18.     return EXIT_SUCCESS;
  19. }
  20.  

Compilare:
  1.  
  2. gcc -Wall -Wextra -g -O0 stack_buffer_overflow.c -o stack_buffer_overflow
  3.  

Ruland folosind mcheck:
  1.  
  2. valgrind --tool=memcheck --leak-check=yes ./stack_buffer_overflow
  3.  

obtinem:
  1.  
  2. ==17691== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 13 from 1)
  3. ==17691== malloc/free: in use at exit: 0 bytes in 0 blocks.
  4. ==17691== malloc/free: 0 allocs, 0 frees, 0 bytes allocated.
  5. ==17691== For counts of detected errors, rerun with: -v
  6. ==17691== All heap blocks were freed -- no leaks are possible.
  7.  

Precum se poate observa, Valgrind nu a raportat nici o eroare ... Asta inseamna cumva faptul ca codul nu are nici o problema ? Nu, inseamna doar faptul ca Valgrind nu a detectat overflow-ul unui buffer alocat pe stiva.
Pur si simplu Valgrind "nu stie" sa detecteze o astfel de problema.

2.6. Detectia de erori privind utilizarea thread-urilor POSIX

Utilitarul helgrind poate fi folosit pentru a detecta erori tipice privind utilizarea thread-urilor POSIX (pthreads).

2.6.1. Race-condition
In exemplul urmator (race_condition.c), vom utiliza helgrind pentru a detecta o problema de tip race-condtion.
  1.  
  2. #include <pthread.h>
  3. #include <unistd.h>
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6.  
  7. int g_counter;
  8.  
  9. void *increment_counter(void* arg)
  10. {
  11.    int i;
  12.    for (i = 0; i < 10; i++)
  13.    {
  14.        ++g_counter;
  15.        sleep(1);
  16.        ++g_counter;
  17.        printf("counter = %d\n", g_counter);
  18.    }
  19.    return NULL;
  20. }
  21.  
  22. int main ()
  23. {
  24.    pthread_t thread1, thread2;
  25.    pthread_create(&thread1, NULL, increment_counter, NULL);
  26.    pthread_create(&thread2, NULL, increment_counter, NULL);
  27.    pthread_join(thread1, NULL);
  28.    pthread_join(thread2, NULL);
  29.    
  30.    return EXIT_SUCCESS;
  31. }
  32.  


In acest exemplu sunt create doua thread-uri: thread1 si thread2.
Cele thread-uri incrementeaza valoarea unei variabile globale, g_counter (fiecare thread incrementeaza g_counter de doua ori, intr-o bucla).
Functia sleep este folosita doar pentru a creste probabilitatea desincronizarii celor doua thread-uri.
Este evident faptul ca cele doua thread-uri sunt in race unul cu celalat, accesul la variabila globala nefiind sincronizat in nici un fel.

Compilare:
  1.  
  2. gcc -Wall -Wextra -g -O0 race_condition.c -o race_condition -lpthread
  3.  


Ruland folosind helgrind:
  1.  
  2. valgrind --tool=helgrind ./race_condition
  3.  

obtinem:
  1.  
  2. ==21286== Possible data race during read of size 4 at 0x804a028 by thread #3
  3. ==21286==    at 0x80484F3: increment_counter (race_condition.c:13)
  4. ==21286==    by 0x402A89B: mythread_wrapper (hg_intercepts.c:194)
  5. ==21286==    by 0x40554FE: start_thread (in /lib/tls/i686/cmov/libpthread-2.9.so)
  6. ==21286==    by 0x414C49D: clone (in /lib/tls/i686/cmov/libc-2.9.so)
  7. ==21286==  This conflicts with a previous write of size 4 by thread #2
  8. ==21286==    at 0x80484FB: increment_counter (race_condition.c:13)
  9. ==21286==    by 0x402A89B: mythread_wrapper (hg_intercepts.c:194)
  10. ==21286==    by 0x40554FE: start_thread (in /lib/tls/i686/cmov/libpthread-2.9.so)
  11. ==21286==    by 0x414C49D: clone (in /lib/tls/i686/cmov/libc-2.9.so)
  12. ==21286==
  13. ==21286== Possible data race during write of size 4 at 0x804a028 by thread #3
  14. ==21286==    at 0x80484FB: increment_counter (race_condition.c:13)
  15. ==21286==    by 0x402A89B: mythread_wrapper (hg_intercepts.c:194)
  16. ==21286==    by 0x40554FE: start_thread (in /lib/tls/i686/cmov/libpthread-2.9.so)
  17. ==21286==    by 0x414C49D: clone (in /lib/tls/i686/cmov/libc-2.9.so)
  18. ==21286==  This conflicts with a previous write of size 4 by thread #2
  19. ==21286==    at 0x80484FB: increment_counter (race_condition.c:13)
  20. ==21286==    by 0x402A89B: mythread_wrapper (hg_intercepts.c:194)
  21. ==21286==    by 0x40554FE: start_thread (in /lib/tls/i686/cmov/libpthread-2.9.so)
  22. ==21286==    by 0x414C49D: clone (in /lib/tls/i686/cmov/libc-2.9.so)
  23. ==21286==
  24. ==21286== Possible data race during read of size 4 at 0x804a028 by thread #2
  25. ==21286==    at 0x804850C: increment_counter (race_condition.c:15)
  26. ==21286==    by 0x402A89B: mythread_wrapper (hg_intercepts.c:194)
  27. ==21286==    by 0x40554FE: start_thread (in /lib/tls/i686/cmov/libpthread-2.9.so)
  28. ==21286==    by 0x414C49D: clone (in /lib/tls/i686/cmov/libc-2.9.so)
  29. ==21286==
  30. ==21286== Possible data race during write of size 4 at 0x804a028 by thread #2
  31. ==21286==    at 0x8048514: increment_counter (race_condition.c:15)
  32. ==21286==    by 0x402A89B: mythread_wrapper (hg_intercepts.c:194)
  33. ==21286==    by 0x40554FE: start_thread (in /lib/tls/i686/cmov/libpthread-2.9.so)
  34. ==21286==    by 0x414C49D: clone (in /lib/tls/i686/cmov/libc-2.9.so)
  35. ==21286==  This conflicts with a previous read of size 4 by thread #3
  36. ==21286==    at 0x80484F3: increment_counter (race_condition.c:13)
  37. ==21286==    by 0x402A89B: mythread_wrapper (hg_intercepts.c:194)
  38. ==21286==    by 0x40554FE: start_thread (in /lib/tls/i686/cmov/libpthread-2.9.so)
  39. ==21286==    by 0x414C49D: clone (in /lib/tls/i686/cmov/libc-2.9.so)
  40. ==21286==
  41. ==21286== Possible data race during read of size 4 at 0x804a028 by thread #2
  42. ==21286==    at 0x8048519: increment_counter (race_condition.c:16)
  43. ==21286==    by 0x402A89B: mythread_wrapper (hg_intercepts.c:194)
  44. ==21286==    by 0x40554FE: start_thread (in /lib/tls/i686/cmov/libpthread-2.9.so)
  45. ==21286==    by 0x414C49D: clone (in /lib/tls/i686/cmov/libc-2.9.so)
  46.  

Valgrind raporteaza faptul ca exista o problema de tip race-condition in fisierul race_condition.c, in functia increment_counter(), la liniile 13, 15 si 16 (adica exact acolo unde variabila globala g_counter este citita respectiv modificata).

Pentru a rezolva aceasta problema, putem folosi un mutex, pentru a asigura accesul sincronizat la variabila g_counter. Exemplul urmator ilustreaza acest lucru (no_race_condition.c):
  1.  
  2. #include <pthread.h>
  3. #include <unistd.h>
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6.  
  7. int g_counter;
  8. /* mutex used to synchronize access to g_counter */
  9. pthread_mutex_t mutex;
  10.  
  11. void *increment_counter(void* arg)
  12. {
  13.    int i;
  14.    for (i = 0; i < 10; i++)
  15.    {
  16.        pthread_mutex_lock(&mutex);
  17.        ++g_counter;
  18.        sleep(1);
  19.        ++g_counter;
  20.        printf("counter = %d\n", g_counter);
  21.        pthread_mutex_unlock(&mutex);
  22.    }
  23.    return NULL;
  24. }
  25.  
  26. int main ()
  27. {
  28.    pthread_t thread1, thread2;
  29.    pthread_mutex_init(&mutex, NULL);
  30.    pthread_create(&thread1, NULL, increment_counter, NULL);
  31.    pthread_create(&thread2, NULL, increment_counter, NULL);
  32.    pthread_join(thread1, NULL);
  33.    pthread_join(thread2, NULL);
  34.    pthread_mutex_destroy(&mutex);
  35.  
  36.    return EXIT_SUCCESS;
  37. }
  38.  

In acest caz, Valgrind nu mai raporteaza nici o problema.

2.6.2. Deadlock
O alta problema des intalnita in dezvoltarea de aplicatii multi-threaded este situatia de deadlock.
Programul de mai jos (deadlock.c) este vulnerabil la aceasta problema.
Se observa aplicarea unuia din cazurile clasice in care se ajunge la deadlock: doua thread-uri achizitioneaza 2 lock-uri (in cazul de fata de tip mutex) in ordine invers.
  1.  
  2. #include <pthread.h>
  3. #include <unistd.h>
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6.  
  7. int g_counter;
  8. pthread_mutex_t mutex1;
  9. pthread_mutex_t mutex2;
  10.  
  11. void *func1(void* arg)
  12. {
  13.     pthread_mutex_lock(&mutex1);
  14.     pthread_mutex_lock(&mutex2);
  15.     ++g_counter;
  16.     sleep(5);
  17.     pthread_mutex_unlock(&mutex2);
  18.     pthread_mutex_unlock(&mutex1);
  19.     return NULL;
  20. }
  21.  
  22. void *func2(void* arg)
  23. {
  24.     pthread_mutex_lock(&mutex2);
  25.     pthread_mutex_lock(&mutex1);
  26.     ++g_counter;
  27.     sleep(5);
  28.     pthread_mutex_unlock(&mutex1);
  29.     pthread_mutex_unlock(&mutex2);
  30.     return NULL;
  31. }
  32.  
  33. int main ()
  34. {
  35.    pthread_t thread1, thread2;
  36.    pthread_mutex_init(&mutex1, NULL);
  37.    pthread_mutex_init(&mutex2, NULL);
  38.    pthread_create(&thread1, NULL, func1, NULL);
  39.    pthread_create(&thread2, NULL, func2, NULL);
  40.    pthread_join(thread1, NULL);
  41.    pthread_join(thread2, NULL);
  42.    pthread_mutex_destroy(&mutex1);
  43.    pthread_mutex_destroy(&mutex2);
  44.    
  45.    return EXIT_SUCCESS;
  46. }
  47.  

Compilare:
  1.  
  2. gcc -Wall -Wextra -g -O0 deadlock.c -o deadlock -lpthread
  3.  

Ruland folosind helgrind:
  1.  
  2. valgrind --tool=helgrind ./deadlock
  3.  

obtinem:
  1.  
  2. ==22570== Thread #3: lock order "0x804A034 before 0x804A04C" violated
  3. ==22570==    at 0x4027CE7: pthread_mutex_lock (hg_intercepts.c:409)
  4. ==22570==    by 0x8048627: func2 (deadlock.c:24)
  5. ==22570==    by 0x402A89B: mythread_wrapper (hg_intercepts.c:194)
  6. ==22570==    by 0x40554FE: start_thread (in /lib/tls/i686/cmov/libpthread-2.9.so)
  7. ==22570==    by 0x414C49D: clone (in /lib/tls/i686/cmov/libc-2.9.so)
  8. ==22570==   Required order was established by acquisition of lock at 0x804A034
  9. ==22570==    at 0x4027CE7: pthread_mutex_lock (hg_intercepts.c:409)
  10. ==22570==    by 0x80485C5: func1 (deadlock.c:12)
  11. ==22570==    by 0x402A89B: mythread_wrapper (hg_intercepts.c:194)
  12. ==22570==    by 0x40554FE: start_thread (in /lib/tls/i686/cmov/libpthread-2.9.so)
  13. ==22570==    by 0x414C49D: clone (in /lib/tls/i686/cmov/libc-2.9.so)
  14. ==22570==   followed by a later acquisition of lock at 0x804A04C
  15. ==22570==    at 0x4027CE7: pthread_mutex_lock (hg_intercepts.c:409)
  16. ==22570==    by 0x80485D1: func1 (deadlock.c:13)
  17. ==22570==    by 0x402A89B: mythread_wrapper (hg_intercepts.c:194)
  18. ==22570==    by 0x40554FE: start_thread (in /lib/tls/i686/cmov/libpthread-2.9.so)
  19. ==22570==    by 0x414C49D: clone (in /lib/tls/i686/cmov/libc-2.9.so)
  20. ==22570==
  21. ==22570== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 60 from 5)
  22.  

Se observa ca Valgrind a detectat situatia de deadlock, datorata faptului ca lock-urile sunt achizitionate in ordine inversa de catre cele doua thread-uri.
7p / 1 votes
Curiosity killed the cat
User avatar
morpheus
Word
 
Joined: 30 Dec 2009
Location: Bucharest, Romania
Status: 54.84

Re: [Linux] - Tutorial Valgrind

Postby Adrian » 05 May 2010, 00:06

Foarte bun tutorialul si atinge un subiect util. Am vazut multi programatori care nu aveau habar sa foloseasca Valgrind si dupa ce au invatat memcheck-ul unul a reusit sa scada consumul de memorie al unui program de la 450 la 300 de mega... 33% e destul de mult deja...

Voiam sa recomand - pentru cei care folosesc Windows - si uneltele puse la dispozitie de Visual Studio (poate sunt unii care cauta o alternativa free pentru Windows - puteti folosi Visual Studio Express, varianta pentru C++ are suport pentru memory leak checking prin metoda descrisa aici - http://msdn.microsoft.com/en-us/library ... 80%29.aspx).

Nu sunt un avocat MS, m-am ciocnit si eu zilele trecute de nevoia de un memcheck pe windows si am gasit alternativa asta. N-am incercat-o inca (sunt inca in dezvoltarea partii care va trebui testata) dar din cate am vazut pare utila. Sper sa revin cu mai multe informatii pentru cei interesati.
0,0p / 0 votes
User avatar
Adrian
Byte
 
Joined: 04 May 2010
Status: 13.5


Return to Tutoriale C++

Who is online

Users browsing this forum: No registered users and 0 guests