Jak na pro­jek­ty v ja­zy­ce C

© Damig, 2004 – 2023
Koncept

Ladění a testování programů
Valgrind – ladění programů s ukazateli

Co valgrind umí

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 valgrind používat

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.

$ valgrind parametryvalgrindu ./vasebinarka vasparametr1 vasparametr2
Spouštění valgrindu. Jako jeho poslední parametry se uvádí spouštěný program se svými parametry. Valgrind pak zajistí předání vašich parametrů vašemu programu. Chcete-li předat valgrindu nějaké jeho vlastní parametry, musíte je zadat před vaší binárkou. Při spouštění bez parametrů valgrind automaticky spouští nástroj memcheck, což je přesně ten modul, který kontroluje práci s ukazateli a dynamicky alokovanou pamětí.

Ukázkový příklad

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á.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
  int n;
  sscanf(argv[1], "%d", &n);

  int *pole = malloc(n*sizeof(int));

  for (int i = 0; i <= n; i++)
    pole[i] = i*i;

  for (int i = 0; i <= n; i++)
    printf("%d ", pole[i]);

  printf("\n");

  return 0;
}
Program s paměťovými chybami. Program se spouští s číselným parametrem na příkazovém řádku. Dále dynamicky alokuje pole zadané velikosti, zapisuje do něj a pak z něj čte. Program je uložen ve zdrojovém souboru test.c a po překladu z něj vznikne binární spustitelný soubor se jménem test (v Linuxu bez koncovky).
$ valgrind ./test
==3540== Memcheck, a memory error detector
==3540== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==3540== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==3540== Command: ./test
==3540==
==3540== Invalid read of size 1
==3540==    at 0x4ECB440: rawmemchr (rawmemchr.S:25)
==3540==    by 0x4EB4C51: _IO_str_init_static_internal (strops.c:44)
==3540==    by 0x4E95B06: __isoc99_vsscanf (isoc99_vsscanf.c:41)
==3540==    by 0x4E95AA6: __isoc99_sscanf (isoc99_sscanf.c:31)
==3540==    by 0x40065C: main (test.c:7)
==3540==  Address 0x0 is not stack'd, malloc'd or (recently) free'd
==3540==
==3540==
==3540== Process terminating with default action of signal 11 (SIGSEGV)
==3540==  Access not within mapped region at address 0x0
==3540==    at 0x4ECB440: rawmemchr (rawmemchr.S:25)
==3540==    by 0x4EB4C51: _IO_str_init_static_internal (strops.c:44)
==3540==    by 0x4E95B06: __isoc99_vsscanf (isoc99_vsscanf.c:41)
==3540==    by 0x4E95AA6: __isoc99_sscanf (isoc99_sscanf.c:31)
==3540==    by 0x40065C: main (test.c:7)
==3540==  If you believe this happened as a result of a stack
==3540==  overflow in your program's main thread (unlikely but
==3540==  possible), you can try to increase the size of the
==3540==  main thread stack using the --main-stacksize= flag.
==3540==  The main thread stack size used in this run was 8388608.
==3540==
==3540== HEAP SUMMARY:
==3540==     in use at exit: 0 bytes in 0 blocks
==3540==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==3540==
==3540== All heap blocks were freed -- no leaks are possible
==3540==
==3540== For counts of detected and suppressed errors, rerun with: -v
==3540== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
První pokus o spuštění programu valgrindem. V tuto chvíli spouštíme program test bez parametru. Jak je vidět, v souboru test.c na řádku 7 byla detekována paměťová chyba. Následně na tomto řádku došlo k havárii.

Pro naše potřeby je zde valgrind až příliš upovídaný a stopuje původ chyby až do hloubi standardní knihovny. Nás samozřejmě zajímá kód z našeho zdrojového textu označený jako (test.c:7), tedy 7. řádek v souboru test.c.

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.

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.

$ valgrind ./test 10
==3945== Memcheck, a memory error detector
==3945== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==3945== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==3945== Command: ./test 10
==3945==
==3945== Invalid write of size 4
==3945==    at 0x400696: main (test.c:12)
==3945==  Address 0x51fd068 is 0 bytes after a block of size 40 alloc'd
==3945==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==3945==    by 0x40066D: main (test.c:9)
==3945==
==3945== Invalid read of size 4
==3945==    at 0x4006C1: main (test.c:15)
==3945==  Address 0x51fd068 is 0 bytes after a block of size 40 alloc'd
==3945==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==3945==    by 0x40066D: main (test.c:9)
==3945==
0 1 4 9 16 25 36 49 64 81 100
==3945==
==3945== HEAP SUMMARY:
==3945==     in use at exit: 40 bytes in 1 blocks
==3945==   total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==3945==
==3945== LEAK SUMMARY:
==3945==    definitely lost: 40 bytes in 1 blocks
==3945==    indirectly lost: 0 bytes in 0 blocks
==3945==      possibly lost: 0 bytes in 0 blocks
==3945==    still reachable: 0 bytes in 0 blocks
==3945==         suppressed: 0 bytes in 0 blocks
==3945== Rerun with --leak-check=full to see details of leaked memory
==3945==
==3945== For counts of detected and suppressed errors, rerun with: -v
==3945== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
Spuštění programu test s parametrem 10. Nyní je vidět chyby na řádcích 12 a 15 a navíc je vidět paměťový únik o velikosti 40 bajtů. Program test na standardní výstup vytiskl řadu jedenácti čísel (0 1 4 9 16 25 36 49 64 81 100). Vzhledem k tomu, že jsme očekávali vypsání deseti hodnot, zřejmě je tu něco špatně.

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.

$ valgrind -q --leak-check=full ./test 10
==5192== Invalid write of size 4
==5192==    at 0x400696: main (test.c:12)
==5192==  Address 0x51fd068 is 0 bytes after a block of size 40 alloc'd
==5192==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5192==    by 0x40066D: main (test.c:9)
==5192==
==5192== Invalid read of size 4
==5192==    at 0x4006C1: main (test.c:15)
==5192==  Address 0x51fd068 is 0 bytes after a block of size 40 alloc'd
==5192==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5192==    by 0x40066D: main (test.c:9)
==5192==
0 1 4 9 16 25 36 49 64 81 100
==5192== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==5192==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5192==    by 0x40066D: main (test.c:9)
Nyní byl použit parametr --leak-check=full pro zobrazení dodatečných informací k paměťovým únikům. Parametr -q zase potlačí zbytečně upovídané výpisy, abychom se mohli soustředit pouze na detekované chyby.

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.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
  int n;

  if (argc != 2)
  {
    fprintf(stderr, "Použijte číselný parametr programu.\n");
    return 1;
  }

  if (sscanf(argv[1], "%d", &n) != 1 || n <= 0)
  {
    fprintf(stderr, "Parametrem programu musí být celé kladné číslo.\n");
    return 1;
  }

  int *pole = malloc(n*sizeof(int));

  for (int i = 0; i < n; i++)
    pole[i] = i*i;

  for (int i = 0; i < n; i++)
    printf("%d ", pole[i]);

  printf("\n");

  free(pole);

  return 0;
}
Program po opravě chyb nalezených s pomocí valgrindu.

Užitečné parametry valgrindu

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.