Krótkie wprowadzenie do GDB

GDB (GNU Project Debugger) jest to program służący do "śledzenia" wykonywania innych programów. Umożliwia on wykonywanie programu krok po kroku, wypisywanie zawartości zmiennych, stosu itd. Dzięki temu łatwiejsze staje się szukanie błędów w programie. Niniejsze wprowadzenie ma za zadanie przedstawienie jego podstawowych komend i możliwości - nie jest kompletnym opisem. Pełny opis programu zawiera GDB User Manual.

W tym dokumencie użyto następujących oznaczeń:

Kompilacja i uruchomienie

Aby umożliwić śledzenie kodu źródłowego programu podczas jego wykonywania, musimy skompilować nasz program dodając przełącznik -g jako parametr dla gcc. Następnie wczytujemy go do debuggera, poleceniem:

$ gdb nazwa_pliku_wykonywalnego

Lista podstawowych poleceń

Polecenia posiadają swoje skróty, jak: list - l, next - n, step - s, break - b, backtrace - bt itd.

Na temat każdego z poleceń gdb możemy przeczytać pomoc:

(gdb) help nazwa_polecenia

Śledzimy wykonanie programu

Użyjemy gdb by prześledzić działanie krótkiego programu: vectors.c (opis - w jego komentarzach). Program bądź to wyrzuca "segmentation fault", bądź też daje wyniki co najmniej niespodziewane i na pierwszy rzut oka nie wiemy dlaczego. Najpierw kompilujemy go odpowiednio:

$ gcc -Wall -g -o vectors vectors.c

Następnie uruchamiamy debugger:

$ gdb vectors

Teraz z linii poleceń gdb, listujemy wczytany program:

(gdb) l     (literka 'l' a nie cyfra '1')

Zobaczyliśmy nagłówek funkcji main i kilka linii dookoła niej. Chcemy teraz obejrzeć trochę więcej - linie od 27 do 50:

(gdb) l 27,50

Ustawimy teraz jeden breakpoint na linii 31 i drugi na 43:

(gdb) b 31
(gdb) b 43

Program pobiera jeden argument wywołania - długość losowanych wektorów, ustawimy więc go teraz:

(gdb) set args 12

Wszystko gotowe - możemy więc uruchomić nasz program:

(gdb) run

Dostajemy komunikat wyglądający mniej więcej tak:

Starting program: /home/username/progs/vectors 12

Breakpoint 1, main (argc=2, argv=0xbffff7d4) at vectors.c:31
31 if((sscanf(argv[1],"%d",&n)==0) || (n<=0)){

Widzimy że program wystartował i zatrzymał się na linii 31 - tam gdzie ustawiliśmy breakpoint. Teraz używając polecenia s (czyli step) możemy przesuwać się - wykonując kod po jednej linii. Po przejściu o jeden krok dalej, sprawdźmy jaka jest wartość zmiennej n:

(gdb) print n

Wygląda na to że jest w porządku. Idziemy więc dalej - poleceniem s. Ups... byliśmy w linii 37 a jesteśmy w 16. Co się stało? Aha, chyba weszliśmy do środka funkcji fmalloc. Wypiszmy zawartość stosu:

(gdb) bt

rzeczywiście - jesteśmy wewnątrz funkcji fmalloc - i widzimy że była wywołana z argumentem size=12. Za nią na stosie widzimy adres powrotu do miejsca w funkcji main z którego został wywołana fmalloc i na samym spodzie: __libc_start_main z biblioteki libc (skąd została wywołana main). Teraz używamy kilkakrotnie s i parzymy co się dzieje - pamięć jest przydzielana poprawnie (możemy sprawdzić wskaźnik przy pomocy print). I wychodzimy z funkcji z powrotem do main.
Ponieważ nie chcemy śledzić funkcji fmalloc dla drugiego jej wywołania - teraz poruszamy się po kodzie używając n (czyli next). Możemy też wyświetlić listę breakpoint-ów poleceniem:

(gdb) info b

Zobaczymy też wówczas ile razy minęliśmy po drodze każdy z nich. Ponieważ przypuszczamy że "ciekawe" rzeczy zaczną się dziać dopiero dalej - możemy teraz kontynuować nasz program aż do następnego punktu przerwania:

(gdb) c       (lub continue).

Wchodzimy teraz w pętli biegnącej po i. Tak... wciskanie kilkadziesiąt razy n - aby znaleźć się w ostatniej iteracji pętli - może się okazać dość żmudne. No ale przecież możemy ustawić sobie jeszcze jeden breakpoint - tym razem warunkowy:

(gdb) b 44 if i >=11

Oznacza to że wykonanie programu zostanie wstrzymane na linii 44, tylko wtedy gdy zmienna i będzie większa lub równa 11 (wiemy gdzie powinna się nasza pętla zatrzymać, a w razie potrzeby - możemy jeszcze raz sprawdzić zmienną n). Teraz pozostaje nam kontynuować wykonanie programu:

(gdb) c

Zatrzymaliśmy się na ostatniej iteracji pętli. Możemy teraz sprawdzać jakie są wartości umieszczone w tablicach używając print. Ponieważ nie znaleźliśmy niczego podejrzanego - wykonujemy program krokowo - aż do następnej pętli. Wchodzimy do niej - i sprawdzamy dokąd przebiega:

(gdb) print m

Ups... a co to za wartość?? I skąd w ogóle się tu wzięła ta zmienna m??!? Aha.. użyliśmy niezainicjowanej zmiennej - która z jakiegoś powodu znalazła się w naszym programie. Powinno być n zamiast m. A ta druga zmienna jest nam przecież do niczego nie potrzebna :-)

Tym razem poszło nam sprawnie - oby nasze następne sesje z debuggerem kończyły się równie szybko i pomyślnie. Czego zarówno wszystkim czytającym jak i sobie - życzy autor niniejszego opracowania.


Opracował Mikołaj Sitarz <sitarz@gmail.com>