Podstawy używania GNU Debugera.

GDB jest jednym z ważniejszych narzędzi programisty w świecie *nixów. Zwłaszcza jeśli doliczy się jego frontendy. Tak czy inaczej, choć mogłoby się wydawać inaczej (ze względu na to, że jest w 100% CLI), jest bardzo potężnym i dość wygodnym narzędziem. Wystarczy poznać kilka podstawowych poleceń, żeby skutecznie odpluskwiać programy. Niestety nie znalazłem w polskim internecie, żadnych tutoriali o GDB, toteż postanowiłem się podzielić listą najczęściej używanych przeze mnie poleceń.

Na początek parę uwag technicznych. GDB, jak wspomniałem, uruchamiamy pod konsolą. Zwyle podajemy w linii polceń nazwę pliku z programem, który chcemy debugować (jeśli tego nie zrobimy, należy wydać polecenie "file <nazwa programu>"). Aby GDB miał dostęp do symbloli takich jak nazwy zmiennych, funkcji, itp, program musi być odpowiednio skompilowany (np. w przypadku gcc i nasma trzeba użyć przełącznika "-g"). Jeśli zamiast polecenia wpisze się pustą linię ("\n"), zostanie wykonanie poprzedniej operacji. Wpisanie w linii poleceń samych białych znaków nie spowoduje żadnego działania.

Gdy mamy załadowany program, z reguły robię jedną z dwóch rzeczy. Pierwszy przypadek to wystąpienie błędu takiego jak sygnały SIGSEGV i SIGABRT. Jest to szybka sprawa. Zaczynamy od razu od wpisania polecenia "run <linia poleceń>". Jeśli program jest interaktywny, działamy tak, żeby wywołać błąd. W momencie jego wystąpienia, GDB o tym poinformuje. Wówczas można użyć kilka razy polecenia "up", które poda miejsce, w którym została wywołana obecna funkcja. Zwykle pierwszych parę wywołań tego polecenia poda parę miejsc poza twoim programem (np. glibc). Przykładowo może wyglądać to tak:

bash-3.1$ gdb a.out
GNU gdb 6.8
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later 
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-slackware-linux"...
(gdb) run
Starting program: /tmp/a.out 
*** glibc detected *** /tmp/a.out: free(): invalid pointer: 0x0804a009 ***
======= Backtrace: =========
/lib/libc.so.6[0xb7de98c4]
/lib/libc.so.6(cfree+0x90)[0xb7ded370]
/tmp/a.out[0x80483b2]
/lib/libc.so.6(__libc_start_main+0xe0)[0xb7d94390]
/tmp/a.out[0x8048321]
======= Memory map: ========
08048000-08049000 r-xp 00000000 03:06 575488     /tmp/a.out
08049000-0804a000 rw-p 00000000 03:06 575488     /tmp/a.out
0804a000-0806b000 rw-p 0804a000 00:00 0          [heap]
b7c00000-b7c21000 rw-p b7c00000 00:00 0 
b7c21000-b7d00000 ---p b7c21000 00:00 0 
b7d72000-b7d7c000 r-xp 00000000 03:06 187042     /usr/lib/libgcc_s.so.1
b7d7c000-b7d7d000 rw-p 00009000 03:06 187042     /usr/lib/libgcc_s.so.1
b7d7d000-b7d7e000 rw-p b7d7d000 00:00 0 
b7d7e000-b7ec4000 r-xp 00000000 03:06 601542     /lib/libc-2.7.so
b7ec4000-b7ec5000 r--p 00146000 03:06 601542     /lib/libc-2.7.so
b7ec5000-b7ec7000 rw-p 00147000 03:06 601542     /lib/libc-2.7.so
b7ec7000-b7ecb000 rw-p b7ec7000 00:00 0 
b7ee9000-b7f05000 r-xp 00000000 03:06 601584     /lib/ld-2.7.so
b7f05000-b7f07000 rw-p 0001b000 03:06 601584     /lib/ld-2.7.so
bf8de000-bf8f3000 rw-p bffeb000 00:00 0          [stack]
ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]

Program received signal SIGABRT, Aborted.
0xb7da8c66 in raise () from /lib/libc.so.6
(gdb) up
#1  0xb7daa571 in abort () from /lib/libc.so.6
(gdb) 
#2  0xb7de196b in __libc_message () from /lib/libc.so.6
(gdb) 
#3  0xb7de98c4 in _int_free () from /lib/libc.so.6
(gdb) 
#4  0xb7ded370 in free () from /lib/libc.so.6
(gdb) 
#5  0x080483b2 in main () at x.c:5
5               free (x+1);
(gdb) 

Drugi, właściwy przypadek jest taki, że coś nie działa tak jak powinno. Wówczas trzeba już znać kilka poleceń i GDB może pokazać więcej mocy. Podstawą w takim wypadku jest ustawienie breakpointa w miejscu w którym zaczniemy analizować program. Używamy do tego polecenia "break <gdzie przerwać>" gdzie parametrem może być nazwa funkcji (np. main) lub numer linijki. W przypadku programów wieloplikowych, numer linii można podać w postaci plik_zrodlowy:numer. Jeśli nie ma nazwy pliku, przyjmowany jest plik z którego pochodzą obecnie analizowane instrukcje (przed uruchomieniem przyjmowany jest plik z punktem startowym - w przypadku c/c++ plik z funkcją main). Jest również możliwość podania adresu.

Gdy jesteśmy w odpowiednim miejscu, czas na przemieszczanie się w programie. Służą do tego, między innymi, polecenia:

  • continue -- program jest kontynuowany. Zatrzyma się w przypadku zakończenia programu bądź natrafienia na breakpoint. Należy zaznaczyć, że linijka wypisana po natrafieniu na breakpoint nie zostaje już wykonana.
  • next -- zostaje wykonana kolejna linijka (ta ostatnio wypisana) po czym wypisywana jest kolejna i program zostaje zatrzymany.
  • step -- działa podobnie do next, z tym, że jeśli wywoływana jest w linii funkcja, której kod jest dostępny (tj. został skompilowany z odpowiednią flagą), gdb przejdzie do tej funkcji i będzie można ją przeanalizować.

Polecenia stepi i nexti działają analogicznie z tym, że wykonują tylko jedną instrukcję procesora.

Czasami uciążliwe jest to, że gdb wypisuje tylko jedną linijkę. Jeśli zachodzi potrzeba zobaczyć ją w szerszym konekście (nie mówiąc o przypadku, gdy w linii jest znak nowej linii) można użyć polecenia "list". Wówczas zostanie wypisana linia która zostanie wykonana jako następna, a także kilka poprzednich i kilka następnych. Jeśli tych kilka linijek to za mało, wpisać jeszcze raz, wówczas wypisanych zostanie jeszcze kilka następnych linii. Do polecenia list można dodać jeszcze numer linii (w takiej formie jak w break, może być również nazwa funkcji, itp). Wówczas zamiast obecnej linii, jako środkową będzie wzięta ta podana. W tym przypadku ponowne wpisanie polecenia zrobi to samo, a wciśnięcie entera wypisze kilka następnych".

Oczywiście można również podglądać wartości zmiennych. Służy do tego funkcja "print <wyrazenie>", gdzie wyrażenie może być praktycznie wszystkim. Może to być nazwa zmiennej, może to być wynik działania (na przykład dereferencji); może być to również rejestr procesora (wówczas należy przedzić go dolarem, np. "$eax"). Jest również możliwość wypisywania takiej informacji za każdym razem, gdy program zostaje zatrzymany. Wówczas używamy funkcji display. Tak wypisywane wartości są ponumerowane. Aby gdb przestał wypisywać tę wartość należy użyć funkcji "undisplay <numer>". Jeśli numer zostanie pominięty, usunięte zostaną wszystkie. W przypadku rejestrów jest również dodatkowa możliwość, funkcja "info registers", w skrócie "i r", która wypisuje stan wszystkich dostępnych rejestrów. Wygląda to mniej więcej tak:

(gdb) i r
eax            0xa      10
ecx            0x804a048        134520904
edx            0x804a020        134520864
ebx            0xb7fb8ff4       -1208250380
esp            0xbfce0d20       0xbfce0d20
ebp            0xbfce0d48       0xbfce0d48
esi            0xb7ff8ce0       -1207989024
edi            0x0      0
eip            0x804858e        0x804858e 
eflags         0x200346 [ PF ZF TF IF ID ]
cs             0x73     115
ss             0x7b     123
ds             0x7b     123
es             0x7b     123
fs             0x0      0
gs             0x33     51

Jest to trochę więcej niż w przypadku print, więc można dodać jeszcze nazwę rejestru. Wówczas wypisany zostanie tylko ten rejestr. Na przykład "i r eflags"

Czasami wypisywanie pojedynczych zmiennych to zdecydowanie za mało. Wówczas można użyć polecenia examine do wypisania zawartości pamięci z danego adresu. Ma ona postać "x/[ile][podstawa][typ] wskaźnik". Szczęgóły w "help x". Na przykład, aby wypisać 12 słów 16 bitowych, w systemie szesnastkowym, spod adresu zapisanego w rejestrze ebp:

(gdb) x/12xh $ebp
0xbfb76bd8:     0x6c48  0xbfb7  0xe390  0xb7e1  0xfce0  0xb7f8  0x8d00  0x0804
0xbfb76be8:     0x6c48  0xbfb7  0xe390  0xb7e1

To już wszystko. Tych poleceń używam najczęściej i one wystarczają do całkiem sprawnego debugowania, choć oczywiście jest to zaledwie mała cząstka możliwości tego programu, wystarczy poczytać pomoc (polecenie "help") do czego zachęcam.


Komentarze do notki “Podstawy używania GNU Debugera.”

  1. Asmo 

    Dzieki za artykul. Mialem w planach nauczyc sie podstaw GDB, ten art mnie zmotywuje i pomoze postawic pierwsze kroki.

  2. dmichael 

    Również dzięki - zabierałem się już do gdb kilka razy, ale nie idzie się uczyć z samej dokumentacji.

Zostaw odpowiedź