Ako radite razvoj za Linux ili neki embeded uređaj, sigurno ste se upoznali sa GDB-om, najrasprostranjenijim debagerom za Linux, a verovatno i najkorišćenijem na svetu. Ovaj alat je za početnike neintuitivan, ali posle određenog vremena programeri se naviknu na njega. Naravno,za ozbiljnog programera samo navići se ne sme biti dovoljno, jer je ovaj alat zaista prepun mogućnosti.
Verujem da ste se do sada upoznali sa najbitnijim komandama GDB-a: c (continue), n (next), b (break), s (step), bt (backtrace) i fr (frame). U ovom članku pričaćemo o manje poznatim, a vrlo korisnim komandama koje GDB nudi u borbi programera sa bagovitim kodom. Dakle, da krenemo.
Prosledite prave opcije kompajleru
Ako ste ikada debagovali optimizovan kod, sigurno ste primetili kako su vrednost promenljivih tokom debagovanja izmešane ili nedostaju, ili kako program skakuće napred-nazad kroz vaš kod. Ovo se dešava jer kompajler prepravlja vaš kod kako bi on bio brži. Međutim, upravo to je ono što otežava debagovanje.
Za neke od ovih problema nema lakog rešenja, ali to ne znači da ne postoji ništa što ne možete da uradite. U nastavku je nekoliko saveta kako da olakšate sebi debagovanje optimizovanog koda. Postoji mogućnost da vam ovi saveti neće biti od pomoći i da ćete morati da smanjite nivo optimizacije vašeg programa da biste mogli da ga debagujete bez teškoća.
- Debagovanje optimizovanog koda i dalje nije kompletirano ni u GCC-u ni u CLANG-u, stoga je preporučljivo da uvek koristite najnovije verzije ovih alata.
- Kod koji se kompajlira za debagovanje se obično kompajlira sa opcijom
-g
koja uključuje simbole za debagovanje u fajlu koji proizvodi kompajler. Međutim, ako često vidite “value optimized out”, poželjno je da uključite dodatne simbole za debagovanje koristeći opciju-ggdb3
umesto-g
. Ova opcija takođe omogućava debagovanje makroa. - Dwarf je format podataka za debagovanje koji mnogi alati koriste za čuvanje podataka o simbolima za debagovanje. Ako to vaš kompajler podržava, prosledite mu opciju
-dwarf-5
da biste koristili najnoviju verziju dwarf formata. - Prosledite opciju
-fno-emit-frame-pointer
kompajleru. Kada uključite ovu opciju, debager će moći lakše da razmota stek i moći ćete da vidite sve funkcije u bektrejsu. U suprotnom, manje funkcije mogu da odsustvuju iz bektrejsa. Ova opcija ima minimalan uticaj na performanse na x86-64 platformi i treba da bude uvek uključena, jer su logovi sa terena mnogo informativniji kada je to slučaj. - Ako i dalje imate problema da debagujete optimizovan kod, smanjite nivo optimizacija sa 3 ili 2 na g. Optimizacioni nivo g (
-Og
) uključuje samo one optimizacije koje ne smetaju pri debagovanju.
Korisne GDB komande
Kako bi vaš rad sa GDB-om bio efikasniji, možete iskoristiti par korisnih GBD komandi iz sledeće liste.
gdb --args
Vaš kod ne radi, a komandna linija je komplikovana (npr. ./my_program --arg1 value1 arg2 value2
). Potrebno je da samo prilepite gdb --args
pre komande i moći ćete da pokrenete program kroz gdb.
gdb --args ./my_program --arg1 value1 arg2 value 2
Možete proslediti vašem programu i varijable okruženja (environment variables), npr.:
ENV_VARIABLE=value gdb --args ./my_program --arg1 value1 arg2 value 2
Zaustavite GDB na prvoj komandi vašeg programa
Pokrenuli ste GDB pomoću gdb --args
i sada biste hteli da postavite brejkpoint, ali je problem što biblioteke nisu učitane i nećete moći da stavljate brejkpointe na komande u vašem programu.
Rešenje je da koristite komandu start
. Komanda start
postavlja privremeni brejkpoint na prvu liniju vašeg programa, a kada pokrenete vaš program, on će se zaustaviti na njoj. Odatle možete da postavite i ostale brejkpointe.
Reading symbols from ./linked_list_test...done.
(gdb) start
Temporary breakpoint 1 at 0x40d770: file linked_list_test.cpp, line 76.
Starting program: /home/ivica/johnysswlab/2020-05-datacaching/linked_list_test
Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe1a8) at linked_list_test.cpp:76
76 int main(int argc, char* argv[]) {
(gdb)
Naredba: until
Naredba until
je jedna kratka i korisna naredba. U slučaju da je vaš program pauziran u debageru, a hteli biste da pomerite debager za određeni broj linija, kucajte until broj_linije
. Debager će nastaviti da radi i zaustaviće se na liniji koju ste specifikovali u komandi.
Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe1a8) at linked_list_test.cpp:76
76 int main(int argc, char* argv[]) {
(gdb) until 90
main (argc=<optimized out>, argv=<optimized out>) at linked_list_test.cpp:90
90 run_test<4, iterations, 4>(my_array);
(gdb)
Naredba: ignore
Dok pokušavate da rešite bag, često se dešava da se bag reprodukuje nakon što je debager stao na brejkpointu tačno određen broj puta. Na primer, problem se javlja u 1027. pozivu funkcije test_func()
Da biste brzo reprodukovali ovakve probleme, koristite naredbu ignore
. Učitajte vaš program kroz GDB i izvršite:
(gdb) break linked_list.h:111
Breakpoint 2 at 0x407538: linked_list.h:111. (12 locations)
(gdb) ignore 2 200
Will ignore next 200 crossings of breakpoint 2.
Naredba ignore preskače brejkpoint zadati broj puta.
Ako želite da vidite koliko puta je vaš program prošao kroz brejkpoint, koristite naredbu info breakpoint
.
(gdb) ignore 2 200
Will ignore next 200 crossings of breakpoint 2.
...
(gdb) info b
Num Type Disp Enb Address What
2 breakpoint keep y <MULTIPLE>
breakpoint already hit 201 times
Naredbe: info locals && bt full
Ukoliko želite da vidite sadržaj svih lokalnih promenljivih, možete to uraditi koristeći naredbu info locals
:
(gdb) info locals
current = 0x611100
block_empty = <optimized out>
next = <optimized out>
prev = 0x0
Ako želite da odštampate lokalne promenljive u stek bektrejsu, koristite bt full
:
(gdb) bt full
#0 linked_list<test_struct<4>, 4>::remove_if<run_test(std::vector<int>&) [with int linked_list_values = 4; int iterations = 1; int struct_size = 4]::<lambda(const test_struct<4>&)> > (this=0x7fffffffe050, condition=<optimized out>) at linked_list.h:116
current = 0x611100
block_empty = <optimized out>
next = <optimized out>
prev = 0x0
#1 run_test<4, 1, 4> (my_array=std::vector of length 50000, capacity 50000 = {...}) at linked_list_test.cpp:66
i = <optimized out>
my_list = {begin = 0x611100, end = 0x6f86d0}
header = "Node size = 4|struct size = 4|"
len = 50000
...
Naredba: bt broj
U slučaju da stek bektrejs sadrži puno funkcija, možete ograničiti ispis na N najugnježdenijih funkcija tako što ćete navesti broj posle naredbe bt
, kao što je u primeru:
(gdb) bt 5
#0 0x000000306e08987b in memcpy () from /lib64/libc.so.6
#1 0x00000000009c4798 in load_input_picture (pic_arg=0x3371fa0, s=0x1ee7b40) at libavcodec/mpegvideo_enc.c:1257
#2 ff_mpv_encode_picture (avctx=0x1ee7710, pkt=0x2788bb0, pic_arg=0x3371fa0, got_packet=0x7fffffffcf6c) at libavcodec/mpegvideo_enc.c:1830
#3 0x000000000082ccea in avcodec_encode_video2 (avctx=avctx@entry=0x1ee7710, avpkt=0x2788bb0, frame=frame@entry=0x3371fa0, got_packet_ptr=got_packet_ptr@entry=0x7fffffffcf6c) at libavcodec/encode.c:296
#4 0x000000000082cfb5 in do_encode (avctx=0x1ee7710, frame=0x3371fa0, got_packet=0x7fffffffcf6c) at libavcodec/encode.c:365
(More stack frames follow...)
Naredba: command
Kada definišete brejkpoint, možete koristiti naredbu command
kojom ćete definisati komande koje treba da se izvrše kada se GDB zaustavi na brejkpointu. Na primer, ako želite da odštampate sadržaj promenljive a potom vratite kontrolu programu, to se radi na ovaj način:
(gdb) break linked_list_test.cpp:52
Breakpoint 2 at 0x407455: linked_list_test.cpp:52. (12 locations)
(gdb) command 2
Type commands for breakpoint(s) 2, one per line.
End with a line saying just "end".
>print len
>continue
>end
Dinamički printf-ovi
GDB nudi mogućnost da dodate printf-ove u program bez potrebe da ga rekompajlirate. Ova mogućnost se zove dinamički printf-ovi. Interno, u GDB-u dinamički printf-ovi realizuju se kao brejkpointovi upareni sa printf-ovima, a izgledaju ovako:
dprintf location, formatting-string, expr1, expr2, …
Location
ukazuje na lokaciju gde biste hteli da stavite vaš dprintf
, slično kao i što se određujelokacija u naredbi break
). Formatting-string, expr1, expr2
itd. imaju isto značenje kao i u slučaju običnih printf-ova, a u nastavku je i primer štampanja korišćenjem dprintf
naredbe.
(gdb) dprintf 145, "First element is %d\n", array_regular[0]
Dprintf 2 at 0x55555555690a: file sorting.cpp, line 145.
...
First element is 0
Zaustavite debager kada memorijska lokacija promeni vrednost
Vočpointi (eng. watchpoints) vam omogućavaju da zaustavite vaš program kada određena memorijska lokacija promeni vrednost. Da biste ih aktivirali, koristite komandu watch
:
(gdb) watch ime-promenljive
Primer:
(gdb) watch current
Watchpoint 3: current
(gdb) c
Continuing.
Watchpoint 3: current
Old value = (linked_list<test_struct<4>, 2>::linked_list_node *) 0x85530e0
New value = (linked_list<test_struct<4>, 2>::linked_list_node *) 0x86706c0
linked_list<test_struct<4>, 2>::remove_if<void run_test<2, 1, 4>(std::vector<int, std::allocator<int> >&)::{lambda(test_struct<4> const&)#1}>(void run_test<2, 1, 4>(std::vector<int, std::allocator<int> >&)::{lambda(test_struct<4> const&)#1}&&) (this=0x7ffffffee260, condition=...) at linked_list.h:116
116 while (current != nullptr) {
(gdb)
GDB je pametan, tako da ukoliko nadgledate promenljivu koja je alocirana na steku, njena adresa će se možda menjati od jednog poziva funkcije do drugog. GDB će uzeti ovo u obzir i prilagodiće adresu koju posmatra zavisno od poziva funkcija. Ako vam ovakvo ponašanje GDB-a smeta, možete ga isključiti na ovaj način:
watch -l ime-promenljive
Naredba: thread apply all
Ako debagujete program sa više niti, sigurno poznajete komandu info threads
koja izlista sve niti u vašem programu. Ali, to nije sve. Komanda thread apply all naredba
će primeniti naredba na svaku nit vašeg programa, npr.:
thread apply all bt // štampa stek bektrejs za svaku nit u programu
thread apply all print $pc // štampa vrednost registra $pc za svaku nit
Ako debagujete problem sa nekim preko telefona ili mejla, prva stvar koju bi trebalo da tražite je izlaz komande thread apply all bt full
. Ova komanda će ispisati stek bektrejsove zajedno sa lokalnim promenljivama za sve niti, što je vrlo koristan alat za prvi korak u debagovanju problema na terenu.
Izvršavanje naredbi iz skripte
GDB naredbe možete čuvati u skripti, i po potrebi ih možete izvršiti. Možete da napravite skriptu kojom se povezujete na razvojnu ploču, konfigurišete GDB ili učitavate brejkpointe iz fajla. Naredbe u skritpi su identične kao i naredbe na GDB komandnoj liniji.
Možete specifikovatiime skripte koja će se izvršiti kada pokrećete GDB korišćenjem opcije -x
.
$ gdb -x gdb-script.txt --args ./my_program
Takođe, možete da izvršite GDB skriptu dok ste u GDB-u korišćenjem naredbe source
:
(gdb) source gdb-script.txt
Naredba: save breakpoints
Novije verzije GDB-a nude naredbu koja će sačuvati sve brejkpointe, vočpointe itd. u fajlu.
(gdb) save breakpoints bp.txt
Saved to file 'bp.txt'.
Ova naredba će sačuvati brejkpointe u fajlu koji je naveden kao argument. Ukoliko želite da povratite brejkpointe koje ste sačuvali ranije, koristite naredbu source
.
(gdb) source bp.txt
Breakpoint 2 at 0x402d2e: file sorting.cpp, line 124.
Breakpoint 3 at 0x403515: file sorting.cpp, line 144.
Brejkpointe možete takođe učitati korišćenjem opcije izvrši skriptu (-x
) u komandnoj liniji, na sledeći način:
$ gdb -x bp.txt --args ./sorting
...
Reading symbols from ./sorting...
Breakpoint 1 at 0x184c: file sorting.cpp, line 125.
Breakpoint 2 at 0x290a: file sorting.cpp, line 148.
(gdb)
Sačuvajte izlaz GDB sesije u fajl
Izlaz vaše GDB sesije možete sačuvati u fajlu koji kasnije možete koristiti da biste se podsetili šta ste radili ili da biste poslali fajl drugima. Izvršite set logging on
kako biste logovali GDB sesiju u fajl gdb.txt
.
.gdbinit
GDB će pri svakom pokretanju izvršiti komande iz fajla .gdbinit
. Ovaj fajl treba da se nalazi u direktorijumu /home/korisnik
. Možete koristiti ovaj fajl da biste prilagodili GDB vašim potrebama: podesili GDB po želji, definisali korisničke komande, logovali sav izaz iz GDB-a u fajl itd. U nastavku je i primer jednog jednostavnog .gdbinit
fajla (uklonite komentare ako ga budete zaista koristili);
set history save on # možete listati kroz naredbe iz prethodnih sesija tasterima gore/dole
set pagination off # ako sav ispis ne staje na ekran, nećete morati da pritiskate enter da biste videli još ispisa
set print pretty on # klase, strukture itd. će biti štampane lepše nego što je to slučaj sa podrazumevanim podešavanjem
set confirm off # nećete morati da potvrđujete naredbe
Na internetu možete naći mnogo drugih korisnih naredbi koje možete staviti u vaš .gdbinit
fajl kako biste olakšali debagovanje. Na primer, možete naći komande koje će vam lepo ištampati sadržaje STL kontejnera.
Fajl .gdbinit
možete čuvati i u bilo kom direktorijumu. Ako GDB pokrenete iz tog direktorijuma, on će učitati taj fajl. Na ovaj način možete izvršiti naredbe pri pokretanju GDB-a koje su korisne za konkretan projekat na kome radite, na primer “poveži se na instancu gdbserver-a”.
Za kraj
Iako su vizuelni debageri lakši za korišćenje, pre ili kasnije ćete morati da se suočite sa GDB-om. GDB je moćan debager sa gomilom sposobnosti, a što ga budete bolje poznavali više ćete ga i voleti. Instaliran je na praktično svakom Linux sistemu kao i na mnogim Windows sistemima. Iako treba vremena da njime ovladate, trud će se kasnije mnogostruko isplatiti.
U sledećem članku ćemo obraditi: GDB-ov Text User Interface koji vam omogućava da lakše navigirate kroz kod dok debagujete i daje mogućnost snimanja i izvršavanja unazad. Takođe, daćemo par primera kako da rešite neke česte probleme pri debagovanju koristeći GDB.
Ovo je sažeta verzija teksta koji je naš kolega Ivica Bogosavljević prvobitno objavio na svom blogu. Ukoliko želite da pročitate još saveta koji će vam koristiti pri debagovanju, celokupni tekst možete pronaći na https://johnysswlab.com/