Jste zde

Ladění pomocí nástroje valgrind - ukázky, jaké nejčastější chyby lze detekovat

Valgrind je open-source nástroj, který slouží především pro ladění paměťových chyb (umí toho víc, např. profilování, ale tím se teď zabývat nebudu). V tomto příspěvu bych se chtěl ukázat jeho možnosti, co se týče detekce problémů při práci s pamětí a to tak, že vždy uvedu jednoduchý příklad kódu (omezím se na jazyk C, ale valgrind zvládá programy napsané v libovolném jazyce, ať už je výsledek kompilovaný či interpretovaný, ovšem hlavně se používá pro programy napsané v C a C++), ve kterém se nachází paměťová chyba a ukázku, jakým výstupem na tuto chybu zareaguje valgrind.

Pár informací na úvod

Všechny příklady jsou uvedeny v jazyce C (podle C99), překládány překladačem gcc (verze 4.3) a následně spuštěny přes valgrind (verze 3.4). Pro smysluplný výstup je třeba překládat s ladícími informaci (u gcc se jedná o přepínač -g). Valgrind je spouštěn s těmito parametry: valgrind --tool=memcheck --leak-check=yes --show-reachable=yes.

A ještě než se do toho pustím, tak bych rád předeslal, že výčet možností tohoto nástroje není rozhodně kompletní - uvedl jsem pouze chyby, o kterých se domnívám, že patří mezi nejčastější.

Ukázky špatné práce s pamětí a výstupy valgrindu

Nejdříve uvedu zdrojový kód programu, poté relevantní výstup valgrindu a nakonec zhodnocení, o jakou chybu se přesně jednalo a případné doplňující informace.

1) Čtění hodnoty neinicializované proměnné

#include <stdio.h>
 
int main() {
    int i;
 
    if (i == 0)
        printf("lucker\n");
 
    return 0;
}
==7287== Conditional jump or move depends on uninitialised value(s)
==7287==    at 0x4004F8: main (vg1.c:6)

Neinicializované lokální proměnné v jazyce C mají po vytvoření nedefinovanou hodnotu (jelikož se vytváří na zásobníku, tak se většinou jedná o smetí, které na tomto místě bylo před vytvořením oné proměnné). Proto je chybou zjišťovat hodnotu takovéto proměnné, protože tato hodnota nám k ničemu není. To stejné platí v případě, pokud alokujete paměť pomocí malloc() - tato funkce alokovanou paměť nijak neinicializuje (pokud ji chcete vynulovat, lze místo malloc() použít calloc()).

2) Přístup přes neinicializovaný ukazatel

int main() {
    int *i;
 
    *i = 0;
 
    return 0;
}
==7478== Use of uninitialised value of size 8
==7478==    at 0x4004A4: main (vg2.c:6)
==7478==
==7478== Invalid write of size 4
==7478==    at 0x4004A4: main (vg2.c:6)
==7478==  Address 0x0 is not stack'd, malloc'd or (recently) free'd
==7478==
==7478== Process terminating with default action of signal 11 (SIGSEGV)
==7478==  Access not within mapped region at address 0x0
==7478==    at 0x4004A4: main (vg2.c:6)

Podobně jako v minulé ukázce, tak i zde je nutné před přístupem na ukazatel nastavit tomuto ukazateli platnou hodnotu (například pomocí malloc()). Pokud tak neučiníte, výsledek při přístupu na takovýto ukazatel je nedefinovaný.

3) Přístup na uvolněnou paměť

#include <stdlib.h>
 
int main() {
    char *i = malloc(10);
 
    free(i);
    *i = 0;
 
    return 0;
}
==7612== Invalid write of size 1
==7612==    at 0x40055F: main (vg3.c:7)
==7612==  Address 0x517a030 is 0 bytes inside a block of size 10 free'd
==7612==    at 0x4C2261F: free (vg_replace_malloc.c:323)
==7612==    by 0x40055A: main (vg3.c:6)

Po uvolnění alokované paměti pro daný ukazatel již přes něj nelze přistupovat. Valgrind správně hlásí, že došlo k neplatnému zápisu na nultý bajt v bloku 10 bajtů, který ovšem byl uvolněn.

4) Přístup mimo hranice alokované paměti

#include <stdlib.h>
 
int main() {
    char *i = malloc(10);
 
    i[10] = 0;
    free(i);
 
    return 0;
}
==7903== Invalid write of size 1
==7903==    at 0x40055A: main (vg4.c:6)
==7903==  Address 0x517a03a is 0 bytes after a block of size 10 alloc'd
==7903==    at 0x4C2391E: malloc (vg_replace_malloc.c:207)
==7903==    by 0x40054D: main (vg4.c:4)

Pokud alokujeme pole o velikosti 10 bajtů, tak index posledního prvku je 9 (tato "chyba o jedna" běžně nastává při procházení pole cyklem for). Přístup za hranici alokované paměti je chyba (valgrind hlásí, že došlo k přístupu na první bajt za alokovaným blokem).

5) Pokus o uvolnění paměti, která nebyla alokovaná dynamicky pomocí malloc()

#include <stdlib.h>
 
int main() {
    char *i = "whoops";
 
    free(i);
 
    return 0;
}
==8534== Invalid free() / delete / delete[]
==8534==    at 0x4C2261F: free (vg_replace_malloc.c:323)
==8534==    by 0x400504: main (vg5.c:6)
==8534==  Address 0x4005fc is not stack'd, malloc'd or (recently) free'd

Paměť, na kterou ukazuje ukazatel a, nebyla alokována dynamicky pomocí malloc(), tudíž pokus o uvolnění takovéto paměti je neplatný.

6) Vícenásobné uvolnění paměti

#include <stdlib.h>
 
int main() {
    char *i = malloc(10);
    char *j = i;
 
    free(i);
    free(j);
 
    return 0;
}
==8603== Invalid free() / delete / delete[]
==8603==    at 0x4C2261F: free (vg_replace_malloc.c:323)
==8603==    by 0x40056B: main (vg6.c:8)
==8603==  Address 0x517a030 is 0 bytes inside a block of size 10 free'd
==8603==    at 0x4C2261F: free (vg_replace_malloc.c:323)
==8603==    by 0x400562: main (vg6.c:7)

Ke každému volání malloc() patří jedno volání free() - ne více, ne méně. Pokus o vícenásobné uvolnění paměti je chybou.

7) Neuvolnění paměti - paměťové úniky (memory leaks)

#include <stdlib.h>
 
int main() {
    char *i = malloc(10);
 
    return 0;
}
==8905== malloc/free: in use at exit: 10 bytes in 1 blocks.
==8905== malloc/free: 1 allocs, 0 frees, 10 bytes allocated.
==8905==
==8905== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1
==8905==    at 0x4C2391E: malloc (vg_replace_malloc.c:207)
==8905==    by 0x4004FD: main (vg7.c:4)

Všechnu dynamicky alokovanou paměť, která již není potřeba, je třeba uvolnit, jinak může dojít k problémům (především u aplikací, které běží delší dobu - u jednoduchých prográmků to není třeba, protože paměť uvolní operační systém, ale patří to k dobrým programátorským návykům). Valgrind hlásí nejen to, kolik proběhlo alokací a dealokací, ale i která paměť nebyla uvolněna.

Přidat komentář