Ladení a testování programu

[  Teoretické okénko  |  Ladení pomocí debuggeru (+NÁVOD)  |  Instrumentace programu  |  Assert - hledání invariantu programu  |  Valgrind - ladení programu s ukazateli  |  Profilování  ]

Teoretické okénko

Cílem programování je vytvorit funkcní program, který splnuje vsechny pozadavky zadání a je odolný vuci ruzným zpusobum pouzívání1. První podmínkou pro splnení techto cílu je dobrý návrh aplikace. Dalsí podmínkou je znalost programovacího jazyka a tretí podmínkou je znalost prostredí v nemz program pobezí. Tímto prístupem se dají eliminovat ty nejzávaznejsí chyby aplikací. Pokud nejsou tyto tri podmínky splneny, nedá se ladením ani testováním moc zachránit.

Kazdý programátor delá chyby. Je potreba se s tím smírit, pocítat s tím a systematicky se chyb zbavovat.

Co jsou a co nejsou programátorské chyby

V anglickém technickém slangu se chybe ríká bug, coz se dá prelozit jako brouk. Etymologie tohoto termínu je jiste zajímavá. Ostatne i v cestine ríkáme, ze "to má mouchy", kdyz neco nefunguje. V programátorské praxi existuje nekolik druhu chyb a je potreba si uvedomit, ze ne za vse muze programátor.

Programátor vetsinou nemá zádný vliv na hardwarové chyby. Existují ale i softwarové chyby, na které programátor nemá vliv. Príkladem muze být napríklad nedostatek volné pameti, kterou muze operacní systém pridelit nasemu programu, nebo chyba v programu, se kterým nás program spolupracuje. Extrémním prípadem pak mohou být chyby v samotném operacním systému. Tyto chyby muzeme nazvat vnejsí chyby, tedy chyby, za nez programátor nenese prímou zodpovednost.

S nekterými z techto chyb ale muze pomoci operacní systém nebo ovladac tím, ze umozní detekovat chybové kódy pro ruzné chybové stavy. Pokud to má smysl (velmi casto to smysl opravdu má), mel by programátor tyto kódy testovat a osetrovat. Cím vetsí skodu muze chyba zpusobit (program pro rízení jaderné elektrárny, projekt do skoly), tím pecliveji by si mel programátor pocínat. Na druhou stranu u programu vytvárených pro vlastní potrebu asi lze být lehkováznejsí. S lehkovázností to ale nelze prehánet, v zamestnání (nebo ve skole) by se to nemuselo vyplatit.

Programátor se muze dopustit syntaktických a sémantických chyb. Syntaktické chyby jsou prohresky proti gramatice pouzívaného programovacího jazyka, takze je pri prekladu odhalí prekladac. Mensí syntaktické chyby odhalí i nekteré chytrejsí editory. Je tudíz dobré soubor pro kontrolu prelozit vzdy po napsání nejakého kousku kódu (cyklus, telo funkce). Pokud je projekt dlouhý, trvalo by to dlouho, proto je výhodné delit programy na moduly a hlavne pouzívat make. Nekteré editory (napr. vim) umí program make spoustet, aniz byste je museli opustit. Vývojová prostredí typicky umoznují spustit preklad príkazem z menu nebo klávesovou zkratkou.

Sématické chyby jsou prohresky proti sémantice, tedy proti pozadovanému chování programu (sémantika = význam). Vsechny tyto chyby principiálne nelze detekovat automaticky (stroji musíme nejprve nejak sdelit, co po nem chceme). Sémantické chyby zpusobují chybnou funkci programu a práve na jejich odhalení se zamerují vsechny techniky hledání chyb, kterými se zde budu dále zabývat.

Chytání brouku

Sémantické chyby v programech lze odhalit ladením, testováním a verifikací. Nikdy nejde o plne automatické techniky, ale o pomucky, které usnadní rucní hledání chyb. Nejlepsím zpusobem, jak se chyb zbavit, je vubec je nedelat. K tomu jsou ovsem potreba zkusenosti. Zkusenosti se hodí i pri samotném hledání chyb. Dále se zde uplatní intuice, abstraktní myslení, predstavivost, ale i stestí. Pokud chcete být dobrým programátorem, ci programátorkou, musíte trénovat a programovat a programovat...

Ladením máme obvykle na mysli systematické hledání chyb pomocí krokování v debuggeru. Debugger je program, který umozní zastavit beh programu po kazdém vykonání jednoho rádku programu nebo na predem definované zarázce (breakpoint). Pokud je ladený program pozastaven, lze si prohlízet ci menit obsah pameti ci konkrétních promenných, stav zásobníku, registru a podobne. Tímto zpusobem kontrolujeme, zda program skutecne delá to, co jsme zamýsleli.

Testování je opet cinnost jejímz primárním cílem je odhalit chyby. V tomto prípade ovsem nesledujeme beh programu krok za krokem, ale snazíme se program spoustet za nejruznejsích (puvodne treba nepredpokládaných) podmínek a sledujeme, jak se s nimi vyporádá. Testovat lze rucne nebo také pomocí ruzných skriptu a sofistikovaných testovacích nástroju. V soucasné dobe existuje celé prumyslové odvetví, které se zabývá testováním aplikací.

Jakkoli nárocným testováním nelze dokázat, ze testovaná aplikace je zcela bez chyb. U nekterých programu vsak lze nekteré trídy chyb detekovat pomocí pocítace, jako napríklad zda se cyklus za urcitých podmínek nestává nekonecným cyklem. Cíle formálne dokázat, ze v programu nejsou urcité trídy chyb, se snazí dosáhnout disciplína, která se nazývá formální verifikace. V této oblasti v soucasné dobe probíhá intenzivní výzkum. Velké firmy pocínaje Microsoftem a konce Boeingem, ci NASA za výzkum v této oblasti utrácí rocne obrovské sumy penez. Zjednodusene receno je princip formální verifikace takový, ze matematicky popíseme vlastnosti, které chceme v softwaru overit (napríklad zivost, dosazitelnost stavu, apod.) a pomocí pocítace se snazíme dokázat, zda daná vlastnost platí ci ne. Blizsí vysvetlení vsak presahuje rozsah toho, co zde chci ríct (klícová slova: formal verification, model checking).

Ladení pomocí debuggeru

Prvním pomocným programem, po kterém programátor chtivý odhalení chyb sáhne, je debugger. Debugger je program urcený pro krokování programu a hledání chyb. Umoznuje sledovat beh programu rádek po rádku a sledovat pri tom hodnoty promenných, registru nebo dokonce vybraných míst v pameti. Kvalitnejsí debuggery umoznují klást do programu zarázky (breakpoints) a preskakovat tak kusy kódu, které nás zrovna nezajímají.

Debugger si lze predstavit jako simulátor instrukcní sady procesoru, tedy jako program, který interpretuje instrukce programu namísto procesoru. Nekteré debuggery takto skutecne fungují a umoznuje to ladit programy pro jiné instrukcní sady, nez zná nás procesor.

Aby mohl debugger správne fungovat, je potreba zdrojový kód prekládat s ladícími informacemi. Jde o nadbytecné informace v kódu programu, které nemají vliv na jeho funkcnost. Debugger je ale umí vyuzít pro správné zobrazení práve vykonávaného rádku zdrojovém kódu (umí si pak spojit binární kód s odpovídajícím rádkem ve zdrojovém souboru), hodnot pouzívaných promenných a ostatních potrebných informací.

Základním debuggerem dodávaným s GCC je rádkový gdb - GNU Debugger. Umí vse potrebné, ale dnesní programátory rozmlsané programy s grafickým rozhraním asi prílis nenadchne. O kvalitách tohoto debuggeru svedcí to, ze vetsina grafických debuggeru pro GCC je pouhou grafickou nádstavbou nad gdb. To znamená, ze gdb musí být nainstalován a tato GUI pak program gdb pouzívají pro vykonávání vsech operací, které nabízejí. Mezi nejpouzívanejsí patrí DDD - Data Display DebuggerInsight.

Obecný postup pri ladení pomocí debuggeru

Tento postup platí s malými odchylkami pro vsechny dnesní debuggery (tedy i ty, které jsou soucástí nejakého vývojového prostredí). Nekteré nabízejí i chytrejsí funkce, ale základní princip pouzívání je totozný.

  1. Prelozte program, který budete ladit s ladícími informacemi.
  2. Spustte prelozený binární soubor pomocí debuggeru. U vetsiny debuggeru stací binárku predat jako parametr debuggeru
    $ ddd ladenyprogram
    
    U integrovaných vývojových prostredí (IDE) toto odpadá, stací zvolit príkaz Debug z menu. Pokud byl program prelozen s ladícími informacemi, debugger zobrazí zdrojový text programu, jinak vetsinou uvidíte kód v assembleru. Pokud je potreba spoustet program s parametry príkazového rádku, u DDD lze tyto parametry pouzít uz pri spoustení.
    $ ddd ladenyprogram param1 param2
    
    Dalsí mozností je sdelit debuggeru parametry pomocí príkazu menu (v DDD je to Program/Run...).
  3. Umístete do zdrojového textu zarázku (breakpoint). Na tomto míste debugger zastaví vykonávání programu a pak bude mozné sledovat jeho stav (promenné, pamet, registry, atd.). Existují dva typy zarázek
  4. Zacnete s krokováním programu (vetsinou Debug/Run). Existuje nekolik módu krokování programu:
  5. Pri zastavení na zvoleném rádku programu lze sledovat
  6. Novejsí debuggery umoznují menit hodnoty promenných. V dalsím kroku pak bude program pracovat s temito novými hodnotami. Tato funkce výrazne usnadnuje ladení, pokud je potreba navodit specifický stav programu, do kterého se lze jinak dostat jen zdlouhavým výpoctem nebo interakcí s programem (napr. kdyz chcete otestovat, co udelá funkce, kdyz jí místo ukazatele predáte null).
  7. Nekteré debuggery obsahují i editor, takze lze do kódu hned vkládat opravy. Nezapomente ale, ze upravený program je potreba pred dalsím ladením znovu prelozit. Napríklad DDD poskytuje tlacítko Edit, kterým se spustí editor (vetsinou vim) a tlacítko Make, kterým se spustí príkaz make.

DDD

Puvodne jsem na tomto míste chtel uverejnit návod k pouzití s obrázky, který by krok za krokem vysvetlil, jak ladit jednoduché i slozitejsí programy. Byla by to vsak zbytecná práce, protoze na domácích stránkách tohoto projektu se nachází podrobnejsí a názornejsí návod, nez jakého bych byl schopen já. Prectete si jej! Je tam na reálném programu názorne predvedeno, jak ladit a vyuzít jeho schopnosti. Tento návod lze získat i jako PDF nebo Postscript. Pokud máte DDD nainstalován na svém pocítaci, najdete tyto návody v adresári /usr/share/doc/ddd/.

Seznam dulezitých odkazu:

Insight

Insight je jednoduchý, ale úcinný debugger, který vytvorili vývojári z RedHatu. Lze jej pouzít i v prostredí MS Windows, pokud pouzíváte MinGW. Pokud pracujete ve Windows s DevC++, doporucuji tento debugger pouzívat místo debuggeru zabudovaného v DevC++, který zatím nefunguje prílis dobre.

Seznam dulezitých odkazu:

Instrumentace programu

Instrumentace programu je principiálne nejjednodussí technika ladení, kterou kazdý zacátecník pouzije asi nejdríve (ale velmi, dokonce VELMI casto ji pouzívají i zkusení programátori). Spocívá v doplnení zdrojového kódu programu o kontrolní výpisy.

int x = 10, y = 5;
printf("x = %d, y = %d\n", x, y); // kontrolní výpis
x = y++ - (x + 3);
printf("x = %d, y = %d\n", x, y); // kontrolní výpis
...

Princip je jednoduchý, ale prekvapive úcinný. Pro ladení programu tímto zpusobem není potreba zádný debugger ani jiný dodatecný software. Na druhou stranu ovsem mohou existovat programy, které jsou schopny instrumentace vyhodnocovat a prezentovat získané údaje názorneji. Dalsí výhodou kontrolních tisku je moznost analyzovat dlouhý beh programu nebo slozité chování, jako napríklad beh paralelního programu. V techto prípadech je pouzití konvencního debuggeru dost problematické.

Pokud si pro ladící výpisy vytvoríme vhodné makro, bude mozné výpisy podle potreby vypínat. V následující ukázce je pro vypínání ci zapínání makra debug pouzit symbol NDEBUG, takze makro bude fungovat podobne jako assert. Príklad lze samozrejme upravit a rozsírit, takze napríklad vytvoríte ruzné skupiny maker pro výpisy, které pujdou vypnout podle potreby (vsechny/vybrané/zádné skupiny). Nebojte se experimentovat a vyzkousejte si to.

#ifdef NDEBUG

#define debug(format, ...) {}

#else

#define debug(format, ...) \
  printf("%s, %s, %d: ", __FILE__, __func__, __LINE__); \
  printf(format, ## __VA_ARGS__)

#endif


// ukázka pouzití
int main(int argc, char **argv)
{
  debug("pocet parametru = %d", argc);
  ...
}

Poznámka: Makra s promenným poctem parametru, stejne jako promenná __func__ a makro __VA_ARGS__ jsou dostupná jen v prekladacích, které znají normu ISO C99. Pokud nevíte, co znamenají symboly __FILE__, ci __LINE__, zkuste jejich význam najít na internetu nebo v manuálových ci info stránkách.

Ackoli instrumentace poskytuje mnozství výhod, nelze ji pouzít vzdy. Nelze ji pouzít v situacích, kdy by kontrolní tisky samy mely vliv na funkcnost programu. Problémy muze pusobit napríklad u nekterých procesu komunikujících pres standardní výstup (zde ale lze tisknout na stderr). Nekdy muze pusobit problémy i to, ze tisk na obrazovku výrazne zpomaluje beh programu, takze se nekteré zákerné chyby projeví az v okamziku, kdy jsou kontrolní výpisy vypnuty. Dá se ovsem ríci, ze v 95% programu instrumentace nebude cinit zádné problémy a umozní najít mnozství chyb.

Assert - hledání invariantu programu

Makro assert neslouzí prímo pro ladení programu. Jeho pouzití lze nazvat jako techniku sebeobranného programování. Hlavní myslenka je jednoduchá - do zdrojového kódu programu umístíte na vhodná místa volání makra assert, jehoz parametrem je logický výraz, o kterém jste presvedcení, ze bude za vsech okolností pravdivý. Pokud behem testování programu dojde k situaci, ze se tento logický výraz vyhodnotí jako nepravdivý, znamená to, ze vás predpoklad neplatí a v programu je chyba. Makro assert proto program ukoncí a vypíse chybové hlásení o tom, na kterém rádku doslo k porusení predpokladu.

void prictiN(int pole[], int delka, int n)
{
  // zavolání funkce se záporným parametrem delka je programátorskou chybou
  assert(delka >= 0);

  for(int i = 0; i < delka; i++)
  {
    pole[i] += n;
  }
}

Definice makra assert se nachází v hlavickovém souboru <assert.h>. Pokud jste si jistí, ze program je dostatecne otestován, lze chování makra assert globálne vypnout tak, ze pred místem vlození hlavickového souboru assert.h vlozíte definici symbolu NDEBUG.

#define NDEBUG
#include <assert.h>

Potom se pri prekladu volání makra nahradí prázdným kódem.

Makro assert se velice casto pouzívá v programech s ukazateli a pro testování parametru funkcí.

void writeList(const TList *list)
{
  assert(list != NULL);
  TItem *item = list->first;
  while (item != NULL)
  {
    printData(item->data);
    item = item->next;
    assert(item != list->first);
  }
}

POZOR! Makro assert nelze pouzívat jako náhradu bezných testu, které jsou soucástí funkcnosti programu. Je potreba si uvedomit, ze funkci makra assert lze vypnout. Pokud na vyhodnocení podmínky závisí funkcnost programu (vyhodnocení podmínky má napríklad vedlejsí efekt), je potreba místo makra assert pouzít test pomocí príkazu if a v prípade detekce chyby tisknout odpovídající chybové hlásení.

Makro assert se rozhodne naucte pouzívat. Jeho správné pouzívání vede ke kvalitnejsím programum s mensím poctem chyb. Krome detekce chyb má makro assert jeste jeden pozitivní efekt. Nutí programátora premýslet nad vlastním kódem, coz nikdy nemuze skodit.

Valgrind - ladení programu s ukazateli

Ukazatele a práce s pametí alokovanou na hromade zpusobují jedny z nejcastejsích a nejzávaznejsích chyb v programech. Potíz s tou cástí pameti, které ríkáme hromada, je v tom, ze tuto cást pameti nás program sdílí s ostatními programy a o její vyuzití musíme zádat operacní systém (operacní systém nám vytvárí tuto abstrakci pohledu na pamet -- ve skutecnosti je to jeste trochu slozitejsí). Pokud se nás program pohybuje v pridelené pameti, je vsechno v porádku. Problém nastává, kdyz se nejakým omylem (napr. indexováním mimo hranici pole nebo chybnou dereferencí) dostaneme mimo tuto bezpecnou zónu. Potom operacní systém vyhodnotí chování naseho programu jako velmi nebezpecné a nás program je nemilosrdne ukoncen. Tuto skutecnost pak systém ohlásí nechvalne známou zprávou "Segmentation fault". Operacní systém tímto chrání sebe a ostatní programy. Pokud by to nedelal, mohly by programy císt nebo prepisovat pamet jiným bezícím programum a celý systém by se stal velmi nestabilním.

Tyto chyby jsou jedny z nejcastejsích. Zároven se tyto chyby obtízne hledají výse zmínenými ladícími nástroji. Jedním z nástroju, které v této situaci dokází pomoci je program valgrind. Tento nástroj slouzí pro dynamickou analýzu programu. To znamená, ze zkoumá bezící programy. Princip jeho funkce je velmi blízký pouzití instrumentace, ale s tím rozdílem, ze nevyzaduje od autora zádné speciální úpravy zdrojového textu.

Valgrind si muzete predstavit jako chytrý interpret instrukcí vaseho programu. Po spustení rídí vykonávání jednotlivých príkazu (instrukcí) vaseho programu a zároven si zaznamenává statistické údaje. Pri pouzití pro nás úcel, tedy ladení programu s ukazateli, si delá statistiku príkazu, které pracují s dynamicky alokovanou pametí (na hromade). Po rádném skoncení programu vytiskne prehlednou tabulku, v níz ukáze, kolik pameti a kolikrát jste celkove alokovali, kolik jste uvolnili a prípadne kolik pameti jste uvolnit zapomneli. Pokud program behem vykonávání provedl nejakou podezrelou nebo chybnou operaci s pametí nebo dokonce v dusledku takové chyby havaroval, vypíse vám valgrind informace o tom, na jakých rádcích se vás program choval spatne.

Jak tedy tento uzitecný nástroj pouzít? Nejdríve jej musíte mít ve svém systému nainstalován. Valgrind funguje pouze na unixových systémech. Pokud pouzíváte nekterou z Linuxových distribucí, urcite najdete instalacní balícek v jejím repositári balícku. Dále pred pouzitím prelozte svuj program a nezapomente pouzít prepínac prekladace -g, který zapíná generování informací pro ladící nástroje. Následne se valgrind spoustí z príkazového rádku:

vas_terminal$ valgrind --tool=memcheck -v --leak-check=yes --show-reachable=yes ./vasebinarka vasparametr1 vasparametr2

Parametrem --tool=memcheck se zapíná nástroj valgrindu, který kontroluje pametové operace (tento parametr je mozné vetsinou vynechat, protoze se zapíná automaticky). Parametr -v zapíná více upovídaný mód. Pokud jej vynecháte, bude valgrind vypisovat informace jen v prípade nalezení chyb a problému. Parametr --leak-check=yes zapne výpis informací o pametových únicích pri skoncení programu. Pametovým únikem se nazývá situace, kdy zapomenete nejakou alokovanou pamet uvolnit nebo prijdete o ukazatel na ni, takze ji uz nejste schopni uvolnit. S parametrem --show-reachable=yes bude valgrind krome bezných pametových úniku hledat i takzvané "neprímé" pametové úniky. To jsou situace, kdy pouzíváte napríklad struktury s ukazateli jako jsou binární stromy nebo lineární seznamy. Pokud prijdete o hlavní prvek takové struktury, hlavicku, je to prímý pametový únik. Vy v tom okamziku ale prijdete i o vsechny prvky, na které je odkazováno z této ztracené struktury. To je neprímý pametový únik.

Pokud pracujete ve vasem programu s dynamicky alokovanou pametí, velmi doporucuji valgrind pouzívat hned od zacátku psaní zdrojového kódu. Ideálne byste meli pouzívat metodu takzvaného inkrementálního programování -- napísete jednu funkci, a hned ji vyzkousíte na vhodné testovací úloze pomocí valgrindu. Az odladíte chyby, napísete dalsí malou funkci, znovu odladíte a takto postupujete, dokud nedokoncíte celý program. Pokud s testováním a valgrindem zacnete az poté, co jste vytvorili celý program, muzete být zdeseni tím, kolik chyb vám valgrind odhalil. Obvykle pak strávíte opravami chyb dvakrát tolik casu nez samotným programováním.

Profilování

Profiler je program, který umí statisticky analyzovat, ve kterých funkcích program tráví nejvíce casu. Tyto funkce pak má smysl optimalizovat. Je zbytecné ztrácet cas optimalizováním funkce, která se pri behu programu vyvolá jednou a program v ní stráví pár milisekund.

Vztah profilování k hledání chyb v aplikacích je neprímý. Pokud program funguje podezrele pomalu, je zrejmé, ze je nekde chyba. Profiler muze pomoci lokalizovat funkce, které nejvíce zdrzují. Na tyto funkce pak muzeme zamerit svou pozornost pri hledání chyb. U malých programu je výhoda zanedbatelná, ale u vetsích aplikací, které se skládají z nekolika modulu a pouzívají mnozství knihoven muze být efektivní lokalizace chyb problémem. Profiler v techto prípadech muze pomoci.

Blizsí informace najdete v samostatné kapitole o profilování.


1. Nemuzete spoléhat, ze se uzivatel bude chovat, jak si prejete. I kdyz uzivateli reknete, ze má zadávat pouze celá císla bez znaménka, vzdy se najde nekdo, kdo zadá císlo -14ab. ;-)

Autor: David Martinek. Poslední modifikace: 29. October 2012. Pokud v tomto dokumentu narazíte na chybu, dejte mi prosím vedet.