Gdb pro normální lidi

15. 10. 2003
Doba čtení: 7 minut

Sdílet

Chcete naplno prožívat svůj profesní i soukromý život, ale dosud vám v tom bránila posvátná hrůza z gdb? Pak jste tu správně :). První johančin článek pro BFU!

Milé děti,

nevím přesně, jaký typ normálních lidí si tento článek bude číst a nerada bych někoho nudila či naopak ochudila o důležité informace, takže si na začátek zahrajeme dobrodružnou forbesu :)

Já, malý linuxák

a) jsem v životě nic nedebugoval a nevím, co to znamená: pokračujte odstavcem 1 – temná sluj
b) jsem už někdy něco debugoval, ale ne v gdb: pokračujte odstavcem 2 – kobka
c) s gdb už něco umím a doufám, že se zde dozvím něco nového: bohužel asi nedozvíte. Vystupujte vpravo ve směru jízdy.
d) jsem ve skutečnosti velký linuxák, snídám gcc, obědvám gdb, večeřím strace a článek si čtu jen proto, že jsem johančin ortodoxní věřící XOR prudič: můžete se teleportovat na libovolné místo v článku, ale pro lepší požitek si ho raději přečtěte celý :)

Odstavec 1 – temná sluj pro malé děti

Vy zřejmě nebudete příliš aktivní programátoři, takže vám gdb bude dobré hlavně k hledání chyb v cizích programech (ne, že byste je poté měli hned opravovat, ale můžete alespoň správně reportovat, kde to spadlo, proč, a vývojář programu bude mít radost, že jeho uživatelé nejsou BFU :)). Debugger je obecně nástroj, který pomáhá hledat zdroje chyb vzniknuvších za běhu programu. Tedy program je syntakticky správně, podařilo se jej zkompilovat, ale za běhu padá, tuhne, hází hlášky či se prostě chová divně. Když vám program lehne, občas ještě stihne zachroptět, ve které funkci k tomu došlo, ale pokud je to jen nějaká pomocná funkcička, těžko zjistíte, co je špatně a od koho dostala případné nekalé argumenty. Debugger vám umožňuje jednak po pádu zjistit víc, po průchodu kterými funkcemi a za jakých okolností k němu došlo, druhak si v něm můžete program v libovolné chvíli stopnout a sledovat jeho chod – jak vypadají vaše oblíbené proměnné, kterými funkcemi procházíme apod.

Odstavec 2 – kobka pro větší děti

Vy jste možná jako já odchovaní Turbo Pascalem, jeho krásným zelenomodrým prostředím a debuggerem v ceně. Nebo možná používáte nějaký jiný obrázkový debugger jinde. Už jste asi někoho viděli pouštět gdb a sami jste si to někdy i omylem zkusili, ale máte z nepřívětivé příkazové řádky trauma na celý život. Také jsem se jí zalekla, až mi kamarád svěřil tajemství – gdb může mít konfigurák usnadňující první krůčky, a to nikoli jeden, dokonce vícero, všechny se však jmenují .gdbinit.

Konfigurák č. 1 se nachází ve vašem homu a můžete si v něm např. definovat vlastní příkazy nebo zapnout historii příkazů, což se dělá následujícně:

set history filename /home/nejake_to_vase_jmeno/.gdb_history
set history save on


Konfigurák č. 2 se nachází přímo v adresáři programu nebo prostě v adresáři, který je pro jeho umístění vhodný (kořenový adresář velkého projektu apod.) Napíšete do něj například název programu, parametry, knihovny, které se mají preloadovat, breakpointy apod., a když pak v onom adresáři zadáte prostý příkaz gdb, spustí se vám vytoužený program s vytouženou konfigurací. Konfigurák pak případně libovolně upravíte, znovu pustíte gdb a nemusíte mu nic z těchto základních údajů zadávat ručně.

Vzorový (můj :)) konfigurák z většího projektu:

#set env LD_PRELOAD=/usr/lib/libefence.so
set env LD_LIBRARY_PATH ./gdk-pixbuf/.libs/:./gdk/.libs/:./gtk/.libs/
file ./tests/.libs/testgtk
b main
b exit
run
#run 2>/dev/null
b gtkfontsel.c:754
#b gtk_button_get_props
#b cursed_theme_draw.c:755
cont


Řádky začínající # jsou komentáře, buď na ozdobu, nebo na demonstraci, co tam ještě můžete napsat. Hned první řádek bude zajímavá odbočka. Knihovna Electric Fence je skvělý nástroj na odhalování chyb práce s pamětí, musíte ji však mít nainstalovánu (není součástí gdb, zato je součástí všech běžných distribucí, takže žádný strach). Funguje tak, že všechnu (oficiálně) nepoužitou paměť zaplní náhodným smetím, takže pokud program střílí, kam nemá, nespadne 3 kilometry odtud, ale poněkud dříve, čímž se snadněji odhalí, kde byla chyba. Preloadovaní efency však používejte s rozmyslem, protože debugování s ní je výrazně pomalejší.

Dále udáváte cestu ke knihovnám už přímo pro váš program a příkazem file určujete binárku, která se má debugovat. Opět se zde pozastavím. Já vím, že nejste blbí, ale zvlášť při debugování cizího programu si nejdříve zjistěte, zda program, který spouštíte a chcete debugovat, je opravdu binárka, nebo jen skriptoidní frontend, v tom případě vystopujte reálnou binárku. Výše použité testgtk se také normálně pouští jen skrz ./tests/testgtk, ale při debugování (a samozřejmě i při straceování apod.) se musíme zanořit až ke kořeni.

Další (důležitá!) věc – máte-li možnost program překompilovat (což typicky máte, je-li váš nebo jsou-li v němu zdrojáky), učiňte tak s použitím debugovacích opšen, typicky nějak takto:

CFLAGS="-ggdb3" ./configure [bla bla..] --enable-debug=yes
make


případně bez Makefilu takto

gcc -ggdb3 -o neco neco.c

configure samozřejmě může být nahrazen i autogenem.sh, podle toho, co používáte. enable-debug (či nějaký podobný přepínač s mírně odlišnou syntaxí) je featura konkrétního programu, to si nějak zjistěte, každopádně vždycky se to hodí, podstatné je to -ggdb3, které přidává do binárek spoustu debugovacích značiček, pomocí nichž gdb získá nejen jména funkcí a proměnných, ale může i říct, ve které části kterého zdrojáku zrovna jste.

Vraťme se ke konfiguráku výše. Příkaz run pouští program, jak vidno, také mu můžeme ještě upřesnit výstup apod. Breakpointyb udávají, kde se má program za běhu v debuggeru zastavit. Jejich seznam je samozřejmě možné upravovat i během debuggování. Názorně vidíte, že je lze popsat všelijak, názvem funkce, číslem řádky v tom kterém zdrojaku (není nutné udávat k němu cestu, stačí název), máme-li s debugovacími opšnami zkompilované i knihovny, můžeme si dávat breakpointy do nich (zde cursed_theme_draw). Posloupnost b main – b nekde:666 – cont znamená, že program se zastaví v mainu, přidá (po nasátí programu, aby věděl kam) další breakpointy a pokračuje, ale to teď nepotřebujete vědět, potřebovali byste to, pokud byste neměli mnou opěvovaný konfigurák :)

Nyní, když máme konfigurák vyroben a program je překompilovaný s debugovacími opšnami, můžeme se vrhnout do lůna neřesti. Napište v adresáři, kde máte .gdbinit, příkaz gdb a sledujte :). Pokud jste zadali všechny cesty správně, měl by se vám pustit váš program jako normálně, pouze trochu pomaleji (případně výrazně pomaleji, preloadujete-li efencu).

Máte-li v programu nějaký oblíbený (deterministický – víte, jak ho vyvolat) crash, vyvolejte ho. A vida, místo běžného Segmentation fault a zbytek si domysli sám je debugger hnedle sdílnější. Dozvíte se alespoň orientačně, kde a na co program slítnul, no a pokud to nebyl zrovna ten nejdrsnější z nejdrsnějších pádů, kdy dojde paměť a přepíší se v ní důležité věci, můžete svou mrtvolku začít pitvat (nemáte-li mrtvolku, stopněte si program v libovolné chvíli buď stisknutím Ctrl+C, nebo preventivním breakpointem a myslete si o něm, že je mrtvolka :), pokračovat v jeho běhu můžete příkazem cont.).

Kouzelný příkaz bt (backtrace) vám vypíše dost dlouhou hromadu funkcí, kterou program před pádem (nebo obecně dosud) prošel, asi takovou:

#0 0xffffe002 in ?? ()
#1 0x42028b93 in abort () from /lib/tls/libc.so.6
#2 0x40412c08 in g_logv () from /usr/lib/libglib-2.0.so.0
#3 0x40412c44 in g_log () from /usr/lib/libglib-2.0.so.0
#4 0x4041058b in g_malloc () from /usr/lib/libglib-2.0.so.0
#5 0x4006cc48 in gdk_pixmap_new (window=0x8127c08, width=14, height=134975320, depth=4) at gdkpixmap-fb.c:138
#6 0x4005a3f6 in gdk_window_begin_paint_region (window=0x8127c08, region=0x812be50) at gdkwindow.c:994
#7 0x4005a039 in gdk_window_begin_paint_rect (window=0x8127c08, rectangle=0xbfffe980) at gdkwindow.c:845


atd. atd.

bitcoin_skoleni

Názorně zde vidíte, k čemu je dobrý kompilační parametr -ggdb3. U vašeho programu, případně knihoven, které byly s tímto parametrem také zkompilovány, máte přímé linky do zdrojáků, strategické parametry funkcí a buď jejich hodnoty, nebo (jde-li o pointer) umístění v paměti. Pokud např. někde uvidíte nejaka_promen­na=0×00000000, je jasné, že předchozí funkce to s předáváním parametrů poněkud neodhadla…

Nu, na navnadění to snad stačilo :), a protože už nemáme mnoho prostoru, necháme si zbytek na někdy příště. Dozvíte se, jak okukovat proměnné, jak kontrolovat, zda vám paměť patří, jak spravovat breakpointy, k čemu je dobrý ten core dump apod. Pokud máte pocit, že jste se zatím dozvěděli houby – já vám to říkala! :)

Autor článku