Analiza programelor utilizand Valgrind
Acest tutorial prezinta un utilitar foarte bun pentru analiza codului:
valgrind.
Valgrind ruleaza
doar pe
Linux, deci aveti nevoie de un sistem
Linux pentru a urmari acest tutorial.
Valgrind este un tool pentru debugging si profiling, foarte util pentru detectarea unei game largi de erori de programare.
Cum functioneaza:
- valgrind este in esenta un emulator software al procesorului host
- cand verificam un executabil utilizand valgrind, rulam acel executabil in mediul simulat
- valgrind contine utilitare pentru debugging si profiling, capabile sa detecteze o gama larga de probleme (precum memory leaks, accesarea memoriei dezlocate, etc ...)
Valgrind contine urmatoarele utilitare:
A)
Memcheck -> detecteaza urmatoarele probleme de accesare a memoriei:
- memory leaks
- folosirea memoriei neinitializate
- dezalocare memoriei de mai multe ori
- accesarea memoriei deja dezalocate
- accese "out of bounds" al zonelor de memorie alocate dinamic
- scrieri si citiri invalide pe stiva
B)
Cachegrind -> profiler pentru cache-uri.
Util pentru a determina in ce masura performanta programului are de suferit din cauza cache miss-urilor
C)
Callgrind -> smilar Cachegrind-ului, dar ofera suport extins pentru callgraph-uri
D)
Massif -> este un profiler pentru heap
Util pentru a urmari consumul de memorie alocata dinamic (pe heap) a programului, numarul de blocuri alocate de catre manager-ul de heap, etc ...
E)
Helgrind -> detecteaza probleme de sincronizare intre thread-uri, in programele multithreaded
Sunt suportate thread-urile POSIX.
Exemple de probleme detectate:
-
deadlock-uri datorate ordinii de lock-ing a mutex-urilor
- accesare unor date de pe mai multe thread-uri fara folosirea unui mecanism de sincronizare (
race condition)
De departe cel mai folosit utilitar este
"Memcheck".
Tutorialul se va axa pe folosirea acestui utilitar.
1.) In primul rand hai sa vedem cum arata raportul oferit de catre valgrind pentru un program care nu are nici o problema in utilizarea memoriei alocate dinamic.
/* no_error.c */
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int *)malloc(sizeof(int));
*p = 1;
printf("Valoarea setata este %d\n", *p);
free(p);
return EXIT_SUCCESS;
}
Compilare:
%> gcc -O0 -g no_error.c -o no_error
Rulare:
%> valgrind ./no_error
==13028== Memcheck, a memory error detector.
==13028== Copyright © 2002-2006, and GNU GPL'd, by Julian Seward et al.
==13028== Using LibVEX rev 1658, a library for dynamic binary translation.
==13028== Copyright © 2004-2006, and GNU GPL'd, by OpenWorks LLP.
==13028== Using valgrind-3.2.1-Debian, a dynamic binary instrumentation framework.
==13028== Copyright © 2000-2006, and GNU GPL'd, by Julian Seward et al.
==13028== For more details, rerun with: -v
==13028==
Valoarea setata este 1
==13028==
==13028== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 8 from 1)
==13028== malloc/free: in use at exit: 0 bytes in 0 blocks.
==13028== malloc/free: 1 allocs, 1 frees, 4 bytes allocated.
==13028== For counts of detected errors, rerun with: -v
==13028== All heap blocks were freed -- no leaks are possible.
Precum vedeti, raportul specifica ca programul face 1 alocare de 4 bytes (ceea ce este corect, deoarece pe sistemul meu tipul int ocupa 4 bytes), si ca memoria alocata este eliberata
2). Identificare erorilor de tip memory-leak
Sa vedem acum felul in care valgrind raporteaza erorile de tip memory leak.
/* memory_leak.c */
#include <stdlib.h>
int main()
{
int *p = (int *)malloc(20 * sizeof(int));
int i;
for (i = 0; i < 20; i++)
p[i] = i;
return EXIT_SUCCESS;
}
Compilare
%>gcc -O0 -g memory_leak.c -o memory_leak
Executie
%>valgrind --leak-check=full --show-reachable=yes ./memory_leak
Output
....
==13422== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 8 from 1)
==13422== malloc/free: in use at exit: 80 bytes in 1 blocks.
==13422== malloc/free: 1 allocs, 0 frees, 80 bytes allocated.
==13422== For counts of detected errors, rerun with: -v
==13422== searching for pointers to 1 not-freed blocks.
==13422== checked 76,392 bytes.
==13422==
==13422== 80 bytes in 1 blocks are definitely lost in loss record 1 of 1
==13422== at 0x4C20A69: malloc (vg_replace_malloc.c:149)
==13422== by 0x4004A9: main (memory_leak.c:5)
==13422==
==13422== LEAK SUMMARY:
==13422== definitely lost: 80 bytes in 1 blocks.
==13422== possibly lost: 0 bytes in 0 blocks.
==13422== still reachable: 0 bytes in 0 blocks.
==13422== suppressed: 0 bytes in 0 blocks.
Observam ca Valgrind raporteaza un memory-leak de 80 de bytes.
Ne mai spune ca blocul de memorie a fost alocat in fisierul memory_leak.c, la linia 5, in functia main
Utilizand aceste informatii, identificam urmatoarea line sursa:
int *p = (int *)malloc(20 * sizeof(int));
in care alocam 80 de bytes (20 * sizeof(int)). Aceasta zona de memorie alocata nu mai este dezalocata in program, ceea ce duce la aparitia unui leak
3) Dezalocarea aceleiasi zone de memorie de mai multe ori
/* double_free.c */
#include <stdlib.h>
int main()
{
int *p = (int *)malloc(sizeof(int));
*p = 1;
free(p);
free(p);
return EXIT_SUCCESS;
}
Compilare
%> gcc -O0 -g double_free.c -o double_free
Executie
%>valgrind --leak-check=full --show-reachable=yes ./double_free
Output
.....
==13998== Invalid free() / delete / delete[]
==13998== at 0x4C2067E: free (vg_replace_malloc.c:233)
==13998== by 0x400519: main (double_free.c:8)
==13998== Address 0x4042030 is 0 bytes inside a block of size 4 free'd
==13998== at 0x4C2067E: free (vg_replace_malloc.c:233)
==13998== by 0x400510: main (double_free.c:7)
==13998==
==13998== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 8 from 1)
==13998== malloc/free: in use at exit: 0 bytes in 0 blocks.
==13998== malloc/free: 1 allocs, 2 frees, 4 bytes allocated.
==13998== For counts of detected errors, rerun with: -v
==13998== All heap blocks were freed -- no leaks are possible.
Raportul ne spune ca am avut o eroare de tip "invalid free".
Ne mai spune ca am avut o singura alocare de memorie (4 bytes), dar doua operatii de dezalocare.
Mai vedem ca eroarea este localizata in fisierul double_free.c, in functia main, la linia 8.
Avand aceste informatii, identificam linia care a cauzat problema ca fiind:
free(p); -> al doilea apel al lui free, care nu trebuia facut si care a cauzat eroarea de tip "double free"
4) Utilizarea memoriei deja dezalocate
/* deallocated_memory_use.c */
#include <stdlib.h>
int main()
{
int *p = (int *)malloc(sizeof(int));
*p = 1;
free(p);
*p = 2;
return EXIT_SUCCESS;
}
In acest program, la linia 8: *p=2, accesez o zona de memorie care a fost deja dezalocata.
Compilare
%>gcc -O0 -g deallocated_memory_use.c -o deallocated_memory_use
Executie
%>valgrind --leak-check=full --show-reachable=yes ./deallocated_memory_use
Output
....
==14615== Invalid write of size 4
==14615== at 0x400515: main (deallocated_memory_use.c:8)
==14615== Address 0x4042030 is 0 bytes inside a block of size 4 free'd
==14615== at 0x4C2067E: free (vg_replace_malloc.c:233)
[/b]==14615== by 0x400510: main (deallocated_memory_use.c:7)[/b]
==14615==
==14615== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 8 from 1)
==14615== malloc/free: in use at exit: 0 bytes in 0 blocks.
==14615== malloc/free: 1 allocs, 1 frees, 4 bytes allocated.
==14615== For counts of detected errors, rerun with: -v
==14615== All heap blocks were freed -- no leaks are possible.
Raportul ne spune ca avem un acces invalid la memorie, de tip scriere, pe 4 bytes. Accesul se face deallocated_memory_use.c, in functia main, la linia 8.
Folosind aceste informatii, identificam linia sursa care a cauzat problema:
*p = 2;
5) Folosirea memoriei neinitializate
/* uninit_variable_use.c */
#include <stdlib.h>
int main()
{
int x;
int y;
if (x > 10)
y = 1;
else
y = 2;
return EXIT_SUCCESS;
}
In acest exemplu, y se va seta la o valoare care depinde de valoarea lui x, care este o variabila neinitializata.
Compilare
%> gcc -O0 -g uninit_variable_use.c -o uninit_variable_use
Executie
%>valgrind --leak-check=full --show-reachable=yes ./uninit_variable_use
Output
....
==15307== Conditional jump or move depends on uninitialised value(s)
==15307== at 0x400450: main (uninit_variable_use.c:8)
==15307==
==15307== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 8 from 1)
==15307== malloc/free: in use at exit: 0 bytes in 0 blocks.
==15307== malloc/free: 0 allocs, 0 frees, 0 bytes allocated.
==15307== For counts of detected errors, rerun with: -v
==15307== All heap blocks were freed -- no leaks are possible.
Raportul ne spune ca, la linia 8, in functia main, in fisierul uninit_variable_use.c, avem un salt conditionat care depinde de o valoare neinitializata.
Folosind aceste informatii, identificam linia sursa care a cauzat problema ca fiind:
if (x > 10)
6) Accesare "out of bounds" pentru zone de memorie alocate dinamic
/* out_of_bounds.c */
#include <stdlib.h>
int main()
{
int *p = (int *)malloc(20 * sizeof(int));
int i;
for (i = 0; i < 20; i++)
p[i] = i;
p[20] = 0;
free(p);
return EXIT_SUCCESS;
}
Accesul out of bounds se face la linia: p[20] = 0
Compilare
%>gcc -O0 -g out_of_bounds.c -o out_of_bounds
Executie
%>valgrind --leak-check=full --show-reachable=yes ./out_of_bounds
Output
....
==17979== Invalid write of size 4
==17979== at 0x40052E: main (out_of_bounds.c:11)
==17979== Address 0x4042080 is 0 bytes after a block of size 80 alloc'd
==17979== at 0x4C20A69: malloc (vg_replace_malloc.c:149)
==17979== by 0x4004F9: main (out_of_bounds.c:5)
==17979==
==17979== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 8 from 1)
==17979== malloc/free: in use at exit: 0 bytes in 0 blocks.
==17979== malloc/free: 1 allocs, 1 frees, 80 bytes allocated.
==17979== For counts of detected errors, rerun with: -v
==17979== All heap blocks were freed -- no leaks are possible.
Raportul ne spune ca a avut loc o scriere invalida pe 4 bytes, la linia 11, in fisierul out_of_bounds.c, in functia main. Scrierea a avut loc in afara zonei de memorie alocate dinamic la linia 5.
Valgrind nu este capabil sa detecteze orice problema privind utilizarea memoriei.
Sa vedem cateva tipuri de erori pe care valgrind nu le poate detecta.
- accesele out of bounds pentru array-urile alocate static.
/* static_out_of_bounds.c */
#include <stdlib.h>
int main()
{
int arr[20];
arr[20] = 0;
return EXIT_SUCCESS;
}
In acest program, arr[20] = 0 genereaza un acces out of bounds
Compilare
%>gcc -O0 -g static_out_of_bounds.c -o static_out_of_bounds
Executie
%>valgrind --leak-check=full --show-reachable=yes ./static_out_of_bounds
Output
==21780== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 8 from 1)
==21780== malloc/free: in use at exit: 0 bytes in 0 blocks.
==21780== malloc/free: 0 allocs, 0 frees, 0 bytes allocated.
==21780== For counts of detected errors, rerun with: -v
==21780== All heap blocks were freed -- no leaks are possible.
Precum se vede, eroarea nu a fost detectata de catre valgrind
- incercarile de alocare dinamica a unei zone de memorie de 0 bytes (alte tool-uri, precum Electric Fence, sunt capabile sa detecteze aceasta eroare)
/* zero_bytes_alloc.c */
#include <stdlib.h>
int main()
{
int *p = (int *)malloc(0);
free(p);
return EXIT_SUCCESS;
}
Compilare
%>gcc -O0 -g zero_bytes_alloc.c -o zero_bytes_alloc
Executie
%>valgrind --leak-check=full --show-reachable=yes ./zero_bytes_alloc
Output
==22020== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 8 from 1)
==22020== malloc/free: in use at exit: 0 bytes in 0 blocks.
==22020== malloc/free: 1 allocs, 1 frees, 0 bytes allocated.
==22020== For counts of detected errors, rerun with: -v
==22020== All heap blocks were freed -- no leaks are possible.
Precum se vede, valgrind nu a detectat nici aceasta problema.