Hlavní navigace

Valgrind - Memcheck

22. 5. 2003
Doba čtení: 13 minut

Sdílet

V této části si ukážeme, co vše umí skin memcheck, a prakticky si to předvedeme na jednom testovacím prográmku. Podíváme se krátce i na to, jak memcheck pracuje.

MEMCHECK

Abychom použili tento skin, můžeme na příkazovém řádku jako argument valgrindu dodat  –skin=memcheck. Není to ale nutné, protože je to defaultní skin.

Memcheck kontroluje všechny zápisy do paměti a čtení z ní, zachytává všechna volání malloc / new / free / delete. Dokáže tedy najít tyto chyby:

  • použití neinicializované paměti
  • čtení / zápis do paměti po jejím uvolnění
  • čtení / zápis za konec alokovaného bloku paměti
  • čtení / zápis do nepatřičných oblastí zásobníku
  • memory leaks
  • neshody v použití malloc / new / new[] vs free / delete / delete []
  • špatné použití knihoven POSIX pthreads API

Popis některěch přepínačů

Pomocí optionu --leak-check=yes ( --leak-check=no) umožníme (zakážeme) hledání memory leaků. Memory leak popisuje situaci, když je někde alokovaný blok, který zatím nebyl uvolněn, ale který není přístupný pomocí žádného pointeru. Taková část paměti už nikdy nemůže být uvolněna programem. Hledání těchto děr v paměti je implicitně vypnuto, protože memcheck má sklon vypisovat desítky chybových hlášení.

Detektor memory leaků může jenom ukazovat bloky, pro které buď nemůže najít žádný ukazatel, nebo najde ukazatel, který směřuje někam doprostřed daného bloku. Tyto bloky jsou podezřelé z memory leaku. Slouží na to přepínač --show-reachable=yes (defaultně je tento přepínač nastaven na  --show-reachable=no).

Pokud klientský program uvolňuje paměť použitím free (v C) nebo delete (C++), není tato paměť hned dostupná pro realokaci. Místo toho je označena jako nedostupná a umístěna do fronty uvolněněch bloků. Účelem tohoto vyčkávání je oddálit další použití této paměti. To zvyšuje šanci, že memcheck bude schopen odhalit chybný přístup k blokům i docela dlouho poté, co už byly uvolněny. Přepínač --freelist-vol=<číslo> udává maximální velikost paměti pro frontu uvolněných bloků v bytech. Defaultní hodnota je 1000000. Zvětšení tohoto číslazvýší množství paměti, kterou použije memcheck, ale může pomoci odhalit přístup k již uvolněné paměti, který by jinak nebylo možné najít.

Přepínač --workaround-gcc296-bugs může nabývat dvou hodnot – yes a defaultní no. Pokud je nastaven na yes, předpokládá valgrind, že zápisy a čtení blízko pod ukazatelem na zásobník – %esp – jsou způsobeny chybami v gcc 2.96, a proto je nehlásí. „Blízko“ je implicitně 256 bytů. gcc 2.96 je defaultní překladač pro některé linuxové distribuce (RedHat 7.X, Mandrake), a proto je možné, že budete chtít tento přepínač použít. Ale nepoužívejte ho, není-li to nezbytné, protože jeho vinou může valgrind přehlédnout skutečné chyby. Další možností je použít gcc / g++, která nepřistupují blízko ukazatele na zásobník (verze 2.95.3 a výše).

Přepínačem –partial-loads-ok můžeme valgrindu říct, jak má naložit s čtením slov (4 byty) z adres, pro které jsou některé byty adresovatelné a některé ne. Pokud je nastaveno yes (defaultní volba), nevyvolají tato čtení chybu adresace. Místo toho se načtené byty, které odpovídají nedovoleným adresám, tváří jako nedefinované. Pokud je příznak nastaven na no, vyvolá čtení takového slova chybu čtení z neplatné adresy.

Memcheck dokáže najít dva druhy chyb: práci s neplatnými adresami a použití nedefinovaných hodnot. Stačí to na to, aby objevil všechny typy chybné práce s pamětí ve vašem kódu. Teď si vysvětlíme, co znamenají které chybové hlášky. Na testování používám soubor memcheck.cpp:

// *************** memcheck.cpp *****************
#include < stdio.h>
#include < stdlib.h>
#include < unistd.h>

int main(int argc, char **argv)
{
int x;
char *p_array = (char *)malloc(10);
p_array = (char *)malloc(10);
int *y = new int[3];

write(1, p_array, 10);

delete(p_array);
delete(y);
printf("x = %d, p_array[10]=%c", x, p_array[10]);
free(p_array);
*p_array = 0;
return 0;
}

Zkompiluji ho příkazem:

zuzka@plexisklo:$~ gcc -ggdb memcheck.cpp -o mem

a valgrind pustím nasledovně:
zuzka@plexisklo:$~ valgrind --leak-check=yes ./mem

Teď se podívejme na výstup:

==607== Memcheck, a.k.a. Valgrind, a memory error detector for x86-linux.
==607== Copyright (C) 2002, and GNU GPL'd, by Julian Seward.
==607== Using valgrind-1.9.6, a program instrumentation system for x86-linux.
==607== Copyright (C) 2000-2002, and GNU GPL'd, by Julian Seward.
==607== Estimated CPU clock rate is 201 MHz
==607== For more details, rerun with: -v
==607==

Hlavička valgrindu vypisuje verzi memchecku a detekovanou rychlost procesoru.

==607== Syscall param write(buf) contains uninitialised or unaddressable byte(s)
==607== at 0x402E3404: __libc_write (in /lib/libc-2.2.5.so)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607== Address 0x40F41060 is 0 bytes inside a block of size 10 alloc'd
==607== at 0x401668B8: malloc (vg_clientfuncs.c:103)
==607== by 0x8049451: main (memcheck.cpp:9)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607==
==607== Invalid read of size 1
==607== at 0x80494A6: main (memcheck.cpp:17)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607== Address 0x40F4106A is 0 bytes after a block of size 10 free'd
==607== at 0x40166B8B: free (vg_clientfuncs.c:185)
==607== by 0x8049503: __builtin_delete (in /home/zuzka/valgrind/mem)
==607== by 0x804948A: main (memcheck.cpp:15)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607==

Chyby čtení a zápisu

Invalid read / Invalid write errors

Tyto chyby nastávají, když program čte nebo zapisuje na adresu, kterou memcheck považuje za neplatnou. V tomto případě program načte jeden byte z adresy 0×40F4106A, která byla volána funkcí free.
Memcheck se snaží zjistit, proč je přístup na danou adresu chybný. Takže pokud ukazuje na blok v paměti, který už byl uvolněn, budete o tom informováni a také se vypíše, kde byl blok uvolněn. Podobně, pokud je to zápis na místo za alokovaným kusem paměti (nejčastější chybou tohoto typu je zápis za poslední index v poli), vypíše se tato chyba a místo, kde byl blok alokován.

==607== Use of uninitialised value of size 4
==607== at 0x4026B413: _IO_vfprintf (in /lib/libc-2.2.5.so)
==607== by 0x402733B5: _IO_printf (in /lib/libc-2.2.5.so)
==607== by 0x80494B7: main (memcheck.cpp:17)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607==

Použití neinicializovaných hodnot

Conditional jump or move depends on uninitialised value(s)
Use of uninitialised va­lue

Tato chyba je hlášena, pokud váš program používá proměnné, které nebyly inicializovány, jinými slovy jejich hodnoty jsou nedefinované. Tady je nalezena ve funkci printf().
Je důležité pochopit, že program může kopírovat neinicializovaná data. Memcheck to povoluje a pamatuje si k nim stopu, ale nevypisuje žádná hlášení. Za chybu se to považuje až tehdy, když se program pokusí tato data použít. V tomto případě memcheck zjistil, že data byla předána _IO_printf a pak _IO_vfprintf, ale to nehlásí. _IO_vfprintf však musí zkoumat hodnotu proměnné x, zda ji lze převést na odpovídající ASCII řetězec, a to je taky ve výpisu memchecku.
Zdroji neinicializovaných dat jsou většinou:
lokální proměnné v procedurách, které nebyly inicializovány (jako v tomto případě)
obsahy alokovaných bloků předím, než je tam něco uloženo. V C++ je malloc „obaleno“ novým operátorem, takže když tvoříte objekt pomocí new, jsou jeho položky neinicializované, dokud je vy (nebo konstruktor) nenaplníte.

==607== Conditional jump or move depends on uninitialised value(s)
==607== at 0x4026B41C: _IO_vfprintf (in /lib/libc-2.2.5.so)
==607== by 0x402733B5: _IO_printf (in /lib/libc-2.2.5.so)
==607== by 0x80494B7: main (memcheck.cpp:17)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607==
==607== Conditional jump or move depends on uninitialised value(s)
==607== at 0x4026B4E8: _IO_vfprintf (in /lib/libc-2.2.5.so)
==607== by 0x402733B5: _IO_printf (in /lib/libc-2.2.5.so)
==607== by 0x80494B7: main (memcheck.cpp:17)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607==
==607== Conditional jump or move depends on uninitialised value(s)
==607== at 0x4026B54E: _IO_vfprintf (in /lib/libc-2.2.5.so)
==607== by 0x402733B5: _IO_printf (in /lib/libc-2.2.5.so)
==607== by 0x80494B7: main (memcheck.cpp:17)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607==
==607== Conditional jump or move depends on uninitialised value(s)
==607== at 0x4026B570: _IO_vfprintf (in /lib/libc-2.2.5.so)
==607== by 0x402733B5: _IO_printf (in /lib/libc-2.2.5.so)
==607== by 0x80494B7: main (memcheck.cpp:17)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607==
==607== Conditional jump or move depends on uninitialised value(s)
==607== at 0x4026B5CB: _IO_vfprintf (in /lib/libc-2.2.5.so)
==607== by 0x402733B5: _IO_printf (in /lib/libc-2.2.5.so)
==607== by 0x80494B7: main (memcheck.cpp:17)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607==
==607== Conditional jump or move depends on uninitialised value(s)
==607== at 0x4026B666: _IO_vfprintf (in /lib/libc-2.2.5.so)
==607== by 0x402733B5: _IO_printf (in /lib/libc-2.2.5.so)
==607== by 0x80494B7: main (memcheck.cpp:17)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607==
==607== Invalid free() / delete / delete[]
==607== at 0x40166B8B: free (vg_clientfuncs.c:185)
==607== by 0x80494C6: main (memcheck.cpp:18)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607== Address 0x40F41060 is 0 bytes inside a block of size 10 free'd
==607== at 0x40166B8B: free (vg_clientfuncs.c:185)
==607== by 0x8049503: __builtin_delete (in /home/zuzka/valgrind/mem)
==607== by 0x804948A: main (memcheck.cpp:15)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607==

Blok je uvolněn nepatřičnou dealokovací funkcí

Invalid free() / delete / delete[]

V C++ je důležité, aby byla paměť delaokována způsobem, který je kompatibilní se způsobem alokace:
Pokud je alokována funkcemi malloc, calloc, realloc, valloc nebo memalign, musí být uvolněna pomocí free
Pokud je alokována s new[], dealokujeme ji funkcí delete[]
Pokud je alokována pomocí new, musíme ji uvolnit funkcí  delete

Když se tyto funkce promíchají, na Linuxu to sice může fungovat, ale problém může nastat na jiné platformě, např. na Solarisu. Takže nejlepší je to pořádně opravit. delete[] musí být spojeno s new[], protože překladač si pamatuje velikost polí a ukazatele na prvek do destruktoru obsahu pole těsně predtím, než se ukazatel vrátí. Proto je velký rozdíl mezi new a new[] a je překvapující, že mnoho překladačů tyto chyby nerozezná.

Nepovolené použití funkce free

Memcheck si pamatuje stopy k blokům, alokovaným ve vašem programu pomocí malloc a new, takže přesně ví, zda je tento argument pro free / delete korektní. Tady se program snaží stejný blok uvolnit dvakrát. Stejně jako u chyb typu „chybné čtení / zápis do paměti“ se memcheck snaží zjistit, jaký význam má tato adresa. Pokud ji najde ve frontě uvolněných bloků, oznámí vám duplicitní uvolnění stejného bloku.

==607== Invalid write of size 1
==607== at 0x80494CD: main (memcheck.cpp:19)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607== Address 0x40F41060 is 0 bytes inside a block of size 10 free'd
==607== at 0x40166B8B: free (vg_clientfuncs.c:185)
==607== by 0x8049503: __builtin_delete (in /home/zuzka/valgrind/mem)
==607== by 0x804948A: main (memcheck.cpp:15)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607==

Na konci vytiskne valgrind shrnutí svých výsledků:

==607== ERROR SUMMARY: 29 errors from 11 contexts (suppressed: 0 from 0)

Bylo nalezeno 29 chyb (v 11 kontextech)
==607== malloc/free: in use at exit: 10 bytes in 1 blocks.
Jak velká paměť se používá při ukončení programu – není uvolněna
==607== malloc/free: 3 allocs, 3 frees, 32 bytes allocated.
==607== For counts of detected errors, rerun with: -v
==607== searching for pointers to 1 not-freed blocks.
==607== checked 3393132 bytes.
==607==
==607== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1
==607== at 0x401668B8: malloc (vg_clientfuncs.c:103)
==607== by 0x804943F: main (memcheck.cpp:8)
==607== by 0x4023914E: __libc_start_main (in /lib/libc-2.2.5.so)
==607== by 0x8049370: (within /home/zuzka/valgrind/mem)
==607==
==607== LEAK SUMMARY:
Výsledek hledání memory leaků:
==607== definitely lost: 10 bytes in 1 blocks.
==607== possibly lost: 0 bytes in 0 blocks.
==607== still reachable: 0 bytes in 0 blocks.
==607== suppressed: 0 bytes in 0 blocks.
==607==

Použití parametrů s nepřiměřenými právy na čtení / zápis u systémových vo­lání

Memcheck kontroluje všechny parametry u systémových volání. Pokud systém potřebuje číst z bufferu poskytovaného vaším programem, memcheck zkontroluje, zda je celý buffer adresovatelný a obsahuje platná data, tj., zda je čitelný. Pokud systémové volání potřebuje do takového bufferu zapisovat, musí být buffer adresovatelný. Po systémovém volání memcheck zaktualizuje administrativní informace o právech paměti, které během něj mohou být změněny.

Na závěr ještě stručně vysvětlím, jak vlastně memcheck pracuje:
Každý byte v paměti má 8 přidružených V (valid-value – platná data) bitů, které říkají, zda má byte definovanou hodnotu, a jeden A (valid-address – platná adresa) bit, který říká, zda má program právo číst/psát na tuto adresu.
Pokud se čte z paměti nebo do ní zapisuje, bere se ohled právě na tyto A bity. Pokud označují neplatnou adresu, vypíše valgrind hlášku o neplatném čtení / zápisu do paměti
Pokud se načítá z paměti do celočíselného registru, jsou příslušné V bity vyzvednuty z paměti a uloženy ve virtuálním procesoru. Nekontrolují se.
Pokud se čte z paměti do registrů s pohyblivou desetinnou tečkou, načtou se z paměti příslušné V bity a okamžitě se zkontrolují. Jsou-li některé z nich neplatné, vznikne chyba neinicializované hodnoty. Tím se předchází kopírování pravděpodobně neinicializované paměti FP registrů (floating point – s pohyblivou desetinnou čárkou) a zjednodušuje práce valgrindu v tom, že nemusí zkoumat stav platnosti dat v registrech FP.
Výsledkem je to, že pokud je zapisován FP registr do paměti, přidružené V bity jsou nastaveny tak, že označují platná data.
Pokud se data v registrech CPU používají na vytvoření adresy, kontrolují se V bity těchto dat (a v případě neplatného bytu je hlášena chyba)
Pokud jsou data v registrech používána za jiným účelem, valgrind vypočítá V bity pro výsledek, ale nekontroluje je. Pokud jsou už jednou V bity zkontrolovány, jsou nastaveny tak, aby označovaly platná data. To zamezí dlouhým řetězům chyb.
Pokud jsou data načtena z paměti, valgrind zkontroluje A bity této oblasti a případně vyšle varování pro čtení z chybné adresy. V tom případě budou V bity označovat platná data, přestože je celá oblast chybná. Toto zdánlivě divné chování redukuje počet matoucích informací, které se dostávají k uživateli. Zabraňuje to vzniku situace, kdy se čte z neadresovatelného místa v paměti, ta obsahuje neplatná data, a výsledkem nebude jenom chyba čtení / zapisování na neplatnou adresu, ale i obrovská množina chyb typu „neinicializovaná hodnota“, a to pro každé jedno použití proměnné. (Je tady ještě nejasný stav, když se čte z bytů, které jsou částečně platné a částečně neplatné. Jak s tím pracovat, bylo popsáno na začátku tototo článku u přepínače –partial-loads-ok).

Memcheck odchytává volání malloc, calloc, realloc, valloc, memalign, free, new a delete. Chová se následovně:
malloc/new: vrácená paměť je označena za adresovatelnou, ale s neplatnými daty. To znamená, že na ni lze psát, ale není možné odtud číst.
calloc: vrácená paměť je adresovatelná a také platná, protože calloc danou oblast vynuluje.
realloc: pokud je nová velikost větší než původní, je nová sekce adresovatelná, ale neplatná, stejně jako u malloc.
pokud je nová oblast menší než původní, zahozená část je označena jako neadresovatelná.
free/delete: dají se zavolat jenom na bloky, které předtím byly alokovány pomocí malloc/calloc/re­alloc nebo hodnotou NULL. Pokud je pointer platný, označí valgrind celou oblast jako neadresovatelnou a uloží blok do fronty uvolněných bloků paměti.

Detekce memory leaků

Memcheck si udržuje přehled o všech blocích paměti kvůli voláním malloc / calloc / realloc / new. Takže když program ukončí svůj běh, ví, které bloky jsou stále adresovatelné, jinými slovy nebyly uvolněny. Ideální situace je, když jsou všechny bloky uvolněny, ale mnoho programů to nedělá.

Pro takové bloky memcheck kontroluje celý adresový prostor procesu a hledá pointery k daným blokům. Mohou nastat tři situace:
Byl nalezen ukazatel na začátek bloku. To obvykle znamená chybu programátora, protože mohl (přinejmenším z principu) daný blok před ukončením programu uvolnit.
Byl nalezen ukazatel na nějaké místo uvnitř bloku (ne začátek). Ukazatel mohl původně ukazovat na začátek a během výpočtu se přesunout. V nějaké proměnné může být „velikost posunutí“, nebo ten ukazatel ukazoval od začátku na toto místo. Memcheck prohlásí tento blok za podezřelý, pravděpodobně tam totiž nastáva memory leak, protože není zřejmé, zda ukazatel na začátek ještě existuje nebo je možné ho nějak vypočítat.
Poslední případ je ten, kdy není nalezen žádný ukazatel do bloku. Pak je blok označen jako obsahující memory leak, protože programátor pravděpodobně zapomněl blok uvolnit.

root_podpora

Memcheck vytiskne zprávu o memory leaks a podezřelých blocích. Pro každý takový blok vypíše i informaci o tom, kde byl alokován. To by nám mělo pomoci zjistit, proč byl pointer ztracen.

To je vše, další zajímavé věci se můžete dozvědět v následujícím dílu :).

Byl pro vás článek přínosný?

Autor článku

Seriál Valgrind