Ukazatele a práce s pamětí alokovanou na hromadě (heap) způsobují jedny z nejčastějších a nejzávažnějších chyb v programech. Potíž s tou částí paměti, které říkáme hromada, je v tom, že ji náš program sdílí s ostatními programy a o její využití musíme žádat operační systém (operační systém nám vytváří tuto abstrakci pohledu na paměť).
Pokud se program pohybuje v přidělené paměti, je všechno v pořádku. Problém nastává, když se nějakým omylem (např. indexováním mimo hranici pole nebo chybnou dereferencí) dostaneme mimo tuto bezpečnou zónu. Potom operační systém vyhodnotí chování programu jako velmi nebezpečné a nemilosrdně ho ukončí. Systém pak ohlásí nechvalně známou zprávu "Segmentation fault".
Operační systém tímto chrání sebe a ostatní programy. Pokud by to nedělal, mohly by programy číst nebo přepisovat paměť jiným běžícím programům a celý systém by se stal velmi nestabilním.
Tyto chyby se obtížně hledají dříve zmiňovanými ladícími nástroji. Program valgrind to umí pomocí zkoumání běžících programů. Princip jeho funkce je velmi blízký použití instrumentace, ale s tím rozdílem, že nevyžaduje od autora žádné speciální úpravy zdrojového textu.
Valgrind si můžete představit jako chytrý interpret instrukcí vašeho programu. Po spuštění řídí vykonávání jednotlivých příkazů (instrukcí) zkoumaného programu a zároveň si zaznamenává statistické údaje. Při použití pro zkoumání programů s ukazateli, si dělá statistiku příkazů, které pracují s dynamicky alokovanou pamětí (na hromadě). Po skončení programu vytiskne přehlednou tabulku, v níž ukáže, kolik paměti a kolikrát jste celkově alokovali, kolik jste uvolnili a případně kolik paměti jste uvolnit zapomněli. Pokud program během vykonávání provedl podezřelou nebo chybnou operaci s pamětí nebo dokonce v důsledku takové chyby havaroval, vypíše valgrind informace o tom, na jakých řádcích se váš program choval špatně.
Jak tedy tento užitečný nástroj použít? Nejdříve jej musíte mít ve svém systému nainstalován. Valgrind funguje pouze na unixových systémech. Pokud používáte některou z Linuxových distribucí, určitě najdete instalační balíček v jejím repositáři balíčků. Dále před použitím přeložte svůj program a nezapomeňte použít přepínač překladače -g, který zapíná generování informací pro ladící nástroje. Následně se valgrind spouští z příkazového řádku tak, že jako jeho parametry předáte svůj program se všemi svými parametry příkazového řádku (viz ukázka 1).
Pokud pracujete s dynamicky alokovanou pamětí, velmi doporučuji valgrind používat hned od začátku psaní zdrojového kódu. Ideálně byste měli používat metodu takzvaného inkrementálního programování. Napíšete jednu funkci, a hned ji vyzkoušíte na vhodné testovací úloze pomocí valgrindu. Až odladíte chyby, napíšete další malou funkci, znovu odladíte a takto postupujete, dokud nedokončíte celý program. Pokud s testováním a valgrindem začnete až poté, co jste vytvořili celý program, můžete být zděšeni tím, kolik chyb valgrind odhalil. Obvykle pak strávíte opravami chyb dvakrát tolik času než samotným programováním.
Programy testujte systematicky a snažte se vyzkoušet všechny eventuality, do kterých se program může dostat. Očekává-li program vstupy od uživatele, testujte program se všemi eventualitami, které může běžný uživatel zadat. Valgrind totiž přes všechny své schopnosti neumí čarovat a části programu, které se při jednom spuštění nevykonají, nemůže zkontrolovat.
Reálné použití valgrindu si nejlépe ukážeme na skutečném programu. V ukázce 2 je program test.c, který obsahuje několik chyb při práci s pamětí. Podíváte-li se na něj zkušeným okem, jistě tyto chyby hned odhalíte. Pokud takové oko nevlastníte, valgrind vám s jejich odhalením pomůže.
Jak už bylo řečeno výše, program test.c je potřeba přeložit s přepínačem -g, aby obsahoval informace pro ladící programy. Po překladu získáme spustitelný program se jménem test. Valgrind by v programu našel chyby i bez toho, ale nebyl by schopen říci, na kterém řádku chyba vzniká.
V ukázce 3 byl program test spuštěn bez parametru pomocí valgrindu. Jak je z výpisu vidět, valgrind na řádku 7 detekoval paměťovou chybu a následně na tomto řádku došlo k havárii. Když se podíváte na odpovídající řádek ve zdrojovém textu, zpracovává se tam pomocí funkce sscanf
parametr příkazového řádku, uložený v poli argv
na indexu 1. Problém je, že jsem v programu neotestoval zda uživatel vůbec nějaké parametry na příkazovém řádku zadal. Toto je tedy první místo zralé na opravu.
V ukázce 4 tedy zkusíme valgrind spustit s parametrem 10. Program by měl alokovat pole délky 10, naplnit je řadou čísel a těchto 10 hodnot by měl následně vypsat na standardní výstup.
V ukázce 4 jsme pomocí valgrindu odhalili hned několik chyb. Hlášení Invalid write of size 4 at 0x400696: main (test.c:12) znamená, že se program na řádku 12 souboru test.c pokouší zapsat 4 bajty (jeden int) do paměti, na kterou nemá právo. To, že program na tomto místě neskončil havárií lze považovat za pouhou náhodu. Prostě jsme se trefili do místa, které nebylo nebezpečné (což ale nevylučuje možnost havárie při některém z budoucích spuštění). Při pohledu na zdrojový text je jasné, že na řádku 12 program zapisuje za hranici alokovaného pole. Podmínka cyklu totiž nesmí být i <= n
, ale musí být i < n
.
Chyba na řádku 15 je podobná té předešlé. Zde se zase z oněch 4 bajtů za koncem pole čte. Protože ale program na tuto paměť stále nemá právo, valgrind to vyhodnotil správně jako chybu. Oprava opět spočívá v úpravě podmínky, tentokrát na řádku 14.
Valgrind zjistil ještě třetí chybu. Když se podíváte na výpis HEAP SUMMARY, valgrind zde hlásí, že v okamžiku ukončení programu bylo stále alokováno 40 bajtů. V programu jsme jednou alokovali a nula krát uvolňovali paměť. Definitivně tedy bylo na konci programu ztraceno 40 bajtů. Je zřejmé, že v programu chybí volání funkce free
, která by uvolnila paměť alokovanou pro pole.
Pokud by nebylo zřejmé, kde k alokaci paměti dochází, valgrind nabízí, že po použití parametru --leak-check=full
řekne více.
Z ukázky 5 jsou patrné obě dříve zjištěné chyby na řádcích 12 a 15. Navíc přibyla informace o tom, že oněch 40 bajtů, o které jsme přišli na konci programu, bylo alokováno na řádku 9 pomocí funkce malloc
.
V tomto jednoduchém příkladu bychom na to přišli sami, ale u větších projektů může k paměťovým únikům docházet v úplně jiných funkcích nebo dokonce modulech, než se na první pohled zdá. Informace, kterou poskytne valgrind je tedy velice cenná, protože skutečné místo paměťového úniku může být jinak velice obtížné najít.
Jak vidíte, pomocí valgrindu jsme odhalili všechny čtyři paměťové chyby, které jsem do původního programu ukryl. Čím větší program budete ladit, tím více oceníte pomoc, kterou vám valgrind poskytne.
Program valgrind má množství přepínačů. Najdete je buďto v manuálové stránce nebo použijte přepínač --help
. Zde se také dozvíte, jaké jsou implicitně nastavené hodnoty jednotlivých přepínačů při spouštění bez parametrů. Mezi přepínače o nichž byste měli vědět patří
-v
zapíná upovídaný mód. Valgrind vám v tomto módu řekne více informací ke každé chybě i o celkovém stavu programu, ale někdy to může být nepřehledné a matoucí.
-q
zapíná tichý mód. V tomto módu valgrind tiskne hlášení jen v případě nalezení chyb a problémů. Když nevytiskne nic, znamená to, že na žádný problém nepřišel.
--tool=memcheck
zapíná modul Memcheck pro testování paměti. Standardně bývá tento modul zapnut. Je ale možné, že v některých Linuxových distribucích bude valgrind nakonfigurován jinak, proto je dobré o tomto přepínači vědět. Mezi další moduly patří cachegrind, callgrind, massif a helgrind.
--leak-check=full
zapíná plnou detekci paměťových úniků. Nejenže odhalí, že k nim dochází, ale je schopen ukázat na místa v programu, kde k nim dochází. Užitečné jako satelitní snímky raketových základen SSSR.
--show-reachable=yes
valgrind bude kromě běžných úniků hledat i takzvané "nepřímé" paměťové úniky. To jsou situace, kdy používáte například struktury s ukazateli jako jsou binární stromy nebo lineární seznamy. Pokud přijdete o hlavní prvek takové struktury, hlavičku, je to přímý paměťový únik. Vy v tom okamžiku ale přijdete i o všechny prvky, na které je odkazováno z této ztracené struktury. To jsou nepřímé paměťové úniky.