Friday, 21 August, 2020 - 9:17am

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/

No content has been promoted yet.