Debugování programů C pod Linuxem/FreeBS

16.12.2003 14:23

Po skončených prázdninách jsem opět vstoupil na akademickou půdu mé milované alma mater. Zde jsem byl, jako každoročně, okamžitě zbaven iluzí. V létě jsem studoval Haskell a Prolog a pln entuziasmu, kterým mne tyto jazyky naplnily jsem vstoupil do přednáškové síně. Že v předmětu Algoritmy a datové struktury budu nucen používat jazyk Pascal jsem věděl a proto jsem nebyl smutný, když se toto potvrdilo. Ovšem od předmětu honosícího se názvem \"Formální jazyky a překladače\" jsem čekal víc. Samozřejmě marně. Hned jsem se dozvěděl, že si užiju jazyka C (což mne mimochodem, docela překvapilo, protože člověka co tyto podmínky určoval znám - znám díki funkcionálním jazykům).

Poté co jsem se přesvědčil, že mne nešálí zrak při čtení zadání semestrálního projektu jsem se ho jal implementovat (sám, ale to je už jiná kapitola, viď jiříku). Projekt spočíval v implementaci interpretu jazyka IFJ03 (tříadresný kód). Toto jsem realizoval ve dvou vrstvách - v první byla lexikální analýza (rozsekání stringu na tokeny) a v druhé kompletní interpret (vyjádřený pomocí konečného automatu). A došlo na lámáni chleba (osobně jsem lámal klávesnici, ale takhle to lépe zní ;) ). Jazyk C ve své neskonalé úžasnosti neobsahuje nic jako správu paměti (natož garbage collector). Měl jsem tedy problém, protože typická iterace v konečném automatu vypadala takto:
get_token() -> promena_1
get_token() -> =
get_token() -> promena_2
get_token() -> +
get_token() -> 'debile'

,kde jsem pro první, třetí, čtvrtý a pátý token alokoval pamět (v modulu lexikální analýzy) o které jsem pak měl v interpretu rozhodnout kdy se má ta která paměť uvolnit (může se uvolnit první, třetí a čtvrtý). To jest věc prakticky nemožná (aspoň tedy pro člověka s mou mozkovou kapacitou). Po pěti hodinách zkoušení (podotýkám, že napsání celého interpretu mi trvalo taky circa 5 hodin) jsem to vzdal - takhle ne! Nakonec jsem implementoval vlastní garbage collector, ale o tom nechci psát. Chci popsat některé nástroje, které jsem použil pro debugování. Průměrný program v jazyce C má zhruba tolik chyb kolik má řádku (včetně komentářů a prázdných řádků) a z toho jde z 90% o chyby související s dynamickou alokací paměti (dangling pointers, free on freed etc.

Úpravy zdrojového kódu (to sice není debugování, ale já hrozně rád buzeruju lidi)Velmi důležitá je důsledná kontrola parametrů. Nikdo vás nenutí mít kontrolní součty položek struktury, ale ověření pointerů na NULL hodnotu bych považoval za samozřejmé. Dále je zde opomíjené makro assert() (feel free to implement your own). Velmi nápomocné je rovněž ověřování translate2czech(\"sanity\")parametrů. Uvedu konrétní příklad - v mém GC mám funkce refer() a derefer() (reference a dereference paměti). Je logické, že pokud referencuji NULL pak je cosi špatně, stejně tak je podezřelé dereferovat paměť, která není referencována. Když takováto situace nastane přicházíke slovu

samotný debuger - gnu debuger pro linux/freebsd
Pokud nám assert (nebo něco obdobného) ohlásí logickou chybu (NULL->something není logická chyba, ale fatální průser) přicházi ke slovu buď watch (sleduje se změna specifikované proměnné) nebo breakpointy (normální jsou docela napiču, naučte se specifikovat podmínku, mnohokrát si objasníte problém aniž byste program vůbec spustili). Řekněme, že máte nějakou funkcí, kterou obhospodařujete chybové stavy (já používám shitdown() ;) ). Nastavíme si tedy na ni breakpoint, pustíme program a mrkneme na stack (příkaz bt). Je docela fajn vidět, které volání přesně způsobilo chybu. To samé lze samozřejmě použít post mortem na core dumpy (tam je ale problém, že hafo věcí je už \"neživých\" - nedostupných). Jelikož jsme ale většinou hloupí (nebo naše zdrojáky nepřehledné ;) ) vetšinou nic nenajdeme a tak se uchýlíme k externí pomoci:

Existuje několik knihoven/programů, které nám pomůžou (popíšu jen ty které jsem použil - pro jiné se mrkněte do příslušné sekce portů vašeho oblíbeného BSD):

ElectricFence
Knihovna, kterou stačí přilinkovat k vaší binárce a stane se zázrak (no, skoro zázrak). Knihovna implementuje vlastní malloc/free procedury, které kontrolují pamět (hence the name). Takže když pustíte free() na nealokovanou pamět tak vám to zařve (a pokud jste v debugeru tak se to stopne) nebo když píšete někam kam byste neměli - můžete tak detekovat první špatné použití pointeru. Vetšinou to totiž napoprvé necoredumpne a až to coredumpne tak už stejně víte hovno. Velká výhoda této knihovny je v tom, že ji najdete snad všude a také to, že je velmi jednoduché ji používat, nevýhodou jsou děsně velké core dumpy (celá paměť) a docela pomalý běh.

Boehm-gc
Další skvělá věcička pro tmavé zimní večery (sorry, ale jsem střízlivý, na což nejsem zvyklý ;( ). Umí zhruba to co ElectricFence, ale k tomu ještě jednu ďábelskou věc! Implementuje vlastní (a docela dobrý) garbage collector. Můžete tedy zapomenout cože to to free() je (boehm používá free() pouze jako nápovědu) a zbavit se ohromného množství chyb a problémů. Garbage collector funguje na principu prohledávání stacku a podobné magie, takže je to doopravdy bezpracné. Dle dokumentace to běží v podstatě všude, nevýhody nevím, pač jsem s tím moc
nedělal.

Valgrind
Všichni jste o něm slyšeli, všichni ho milujete. Já ne (resp. ne tak docela). Valgrind je implementace virtuálního stroje pro operační systém Linux (viděl jsem patch pro NetBSD). Simuluje veškeré instrukce, které šahají do paměti (takže zhruba všechny) a pokud se šáhne někam kam nemá tak to nahlásí, dále umí dělat profilování s ohledem na cache-fitting. Jedná se o docela fajn program. K mé výtce. Valgrind by neměli používat začátečníci - naučí je hrozné věci. Nevím jak ostatní, ale já jsem se vždy při používáni valgrindu nachytal ve smyčce
typu:

valgrind ./a.out
catch first error
vi that_program.c
make a workaround
make
loop

Valgrind vám podá perfektní informace, které má člověk tendenci zpracovávat sériově a nevidět mezi nimi spojení (které bývá to hlavní). Valgrind je super, ale měl by se užívat s rozmyslem. Ono je možná pravda, že ideálníje debugovat na papír.

Další věcí, která může člověka docela nasrat, je když jde o chybu gcc a ne jeho. Uvedu příklad:
necrophilia ifj-project$ cvs diff -r 1.19 -r 1.20
cvs diff: Diffing .
Index: interpret.c
===================================================================
RCS file: /home/roman/school/IFJ/ifj-CVS/ifj-project/interpret.c,v
retrieving revision 1.19
retrieving revision 1.20
diff -r1.19 -r1.20
22d21
< * There is (really_used != count) after force_gc() bug
25c24
< * $Id: interpret.c,v 1.19 2003/12/06 10:53:06 roman Exp $
---
> * $Id: interpret.c,v 1.20 2003/12/06 11:55:28 roman Exp $

necrophilia ifj-project$ cvs log -r1.19:1.20 interpret.c
....
revision 1.20
date: 2003/12/06 11:55:28; author: roman; state: Exp; lines: +1 -2
really_used != count after force_gc () bug suddenly disappeared...
----------------------------
revision 1.19
date: 2003/12/06 10:53:06; author: roman; state: Exp; lines: +48 -14
Comments.

dost brutální oprava řekl bych ;) Podotýkám, že od tohoto commitu to už perfektně funguje a (really_used == count) holds after force_gc(). (v češtině byste to řekli jak?

GCC je velmi chybové a tak se někdy stávají věci podivné ;( Ovšem v 99.9% je chyba na vaší straně a tak prosím nebombardujte gcc mailing listy :-) (btw: ta hodina mezi commity je drsná, co

Závěr
Jak vidno s Céčkem je více problémů, než radostí a užitek z něho je provázen trpokostí a nadávkamy. Máte tedy dvě možnosti - buďto po přečtení tohoto článku vypálite sérii \"pkg_add -r efence boehm-gc\", nebo půjdete do knihovny a přečtete si knížku o nějakém rozumném jazyce. Myslím, že druhá mžnost je lepší pro nás pro všechny...

neologism

P.S. ti zmrdaní kreténi, co si teď chtějí začít honit ego nad tím, že buďto neumím českou gramatyku (feed the trolls) nebo že jsem napsal tendenční článek o hovně, ať si jdou radši užít poslední okamžiky života (doprdel tak to nečtěte, když se vám to nelíbí), protože se už brzo naseru a všechny je vyvraždím...neologism