"Zwykłe" debugowanie - Linux

Autor: Paweł Kaczan

Spis treści


Wstęp


Programy takie jak gdb dają możliwość wglądu "w głąb" działającego programu lub analizy stanu programu w chwili jego zakończenia w wyniku nieprzewidzianego błędu.

Gdb pozwala na:

  1. uruchomienie programu, uprzednio definiując wszystkie parametry środowiska uruchamiania, które mogą wpływać na jego działanie;
  2. zatrzymanie działania programu w momencie spełnienia zdefiniowanego warunku;
  3. analizę tego, co się stało w momencie zatrzymania programu;
  4. zmianę elementów programu.

Inne debugery pod Linuksa

Dla Linuksa dostępnych jest kilka interaktywnych debugerów. Bezdyskusyjnym standardem jest tu gdb, na podstawie którego zostanie omówione odpluskwianie pod Linuksem.

Oprócz gdb istnieje kilka innych debugerów pod Linuksa, a do najważniejszych należą:

 


Uruchamianie gdb


Gdb może być uruchamiany na wiele sposobów. Podstawowym sposobem jest wpisanie gdb. Aby zakończyć działanie programu należy wpisać quit lub nacisnąć sekwencję klawiszy C-d.

Możliwe jest uruchamianie gdb z wieloma różnymi argumentami. Niektóre z nich to:

gdb program - pozwala na uruchomienie gdb z określonym programem wykonywalnym, na którym operujemy;

[user@localhost gdb]$ gdb przyklad1 

GNU gdb Red Hat Linux (6.3.0.0-1.21rh) 

Copyright 2004 Free Software Foundation, Inc. 

GDB is free software, covered by the GNU General Public License, and you are 

welcome to change it and/or distribute copies of it under certain conditions. 

Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. 

This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db library "/lib/libthread_db.so.1".

gdb program core - ponadto określamy plik zrzutu pamięci (core);

gdb program pid - określamy działający proces, który będzie analizowany. 


Uruchamianie programów pod gdb


Kompilowanie programów w celu odpluskwiania

Aby możliwe było uruchomienie programu pod gdb w celu odpluskwiania, wymagane jest wygenerowanie odpowiednich danych na etapie kompilacji programu.

Kompilator uruchomiony z opcją '-g' generuje plik obiektowy zawierający informację dla gdb. Oprócz kodu wynikowego programu plik wykonywalny zawiera informację na temat wszystkich obiektów wykorzystywanych w programie, takich jak zmienne i funkcje, a także zależności, jakie występują między numerami linii w pliku źródłowym programu, a adresami w kodzie wynikowym.

[user@localhost gdb]$ gcc -g -o przyklad1 przyklad1.c      

Programy przeznaczone dla docelowego odbiorcy kompilowane są zazwyczaj z opcją '-O', która wymusza optymalizację kodu. Gcc wspiera kompilację z opcją '-g' i, jednocześnie,  z opcją '-O'. 

Analizując programy skompilowane z opcją '-O' należy pamiętać, że w wyniku optymalizacji kod źródłowy programu zostaje zreorganizowany tak, by zapewnić większą wydajność. Może to prowadzić to do rozbieżności między kodem źródłowym a wynikowym programu. Za przykład może posłużyć sytuacja, gdy w programie zdefiniowaliśmy zmienną, której nigdy nie użyliśmy. Zoptymalizowany kod wynikowy nie będzie zawierał informacji na temat tej zmiennej.


Uruchamianie programu

run - polecenie pozwalana na uruchomienie programu. Należy jednak uprzednio zdefiniować nazwę programu wykorzystując jeden ze sposobów:

  1. jako argument wywołania gdb;
  2. polecenie file filename lub exec-file [filename] pozwala na określenie pliku, na którym operuje gdb. Jeśli nie podano ścieżki do pliku, to gdb szuka pliku o podanej nazwie w katalogach ścieżki PATH..

(gdb) run 

Starting program: /home/user/gdb/przyklad1 

Reading symbols from shared object read from target memory...done. 

Loaded system supplied DSO at 0xf4b000 

 

Breakpoint 1, density (x=0, y=0) at przyklad1.c:39 

39          result=eval(x,y);

Wywołanie programu uzależnione jest od szeregu informacji, które proces otrzymuje w momencie wywołania. Gdb zapewnia możliwość definiowania tych informacji, które można podzielić na cztery kategorie:

start - W zależności od języka programowania różne są nazwy procedury, która ma być wykonywana w momencie uruchamiania programu. Polecenie start pozwala określić, od którego miejsca w kodzie rozpoczyna się wykonywanie programu.

 Argumenty programu

set args - pozwala sprecyzować argumenty, które zostaną użyte przy następnym uruchomieniu programu. Jeśli set args nie otrzyma argumentów, run wywołuje program bez argumentów.

show args - pokazuje argumenty, z jakimi zostanie wywołany program.

Środowisko programu

Środowisko działania programu składa się ze zbioru zmiennych środowiskowych wraz z ich wartościami.

path directory - dodaje directory do PATH. Wyświetlenie zawartości PATH możliwe jest przy użyciu komendy show paths.

set environment varname [=value] - ustawia wartość varname na value. Jeśli nie podano wartości varname, przyjmuje ona wartość nieokreśloną. Wyświetlenie zmiennych środowiskowych możliwe jest przy użyciu komendy show environment [varname]. Polecenie unset environment varname usuwa varname ze środowiska wywołania programu.

Katalog bieżący

cd directory - zmienia katalog bieżący na directory. Komenda pwd wyświetla katalog bieżący dla gdb.

Standardowe wejście i wyjście

Domyślnie program uruchamiany pod gdb ma to samo wejście i wyjście co gdb. Przekierowanie możliwe jest przy użyciu przekierowania dostarczanego przez powłokę wywołania gdb oraz polecenia run. Na przykład:

(gdb) run > outfile


Odpluskwianie działającego procesu

attach process-id - polecenie pozwala na podpięcie gdb do działającego procesu uruchomionego poza gdb. Polecenie za argument przyjmuje ID procesu. Zazwyczaj znalezienie ID procesu w systemach Unix możliwe jest przy użyciu polecenia ps.

W wyniku użycia polecenia attach gdb odnajduje program działającego procesu (korzysta przy tym z programu określonego przy pomocy polecenia file, a jeżeli polecenie nie zostało wykorzystane, rozpoczynając poszukiwania od bieżącego katalogu, potem katalogów ze źródłami), a następnie wstrzymuje jego działanie. Od tej chwili możliwe jest analizowanie i modyfikowanie podpiętego procesu przy użyciu komend gdb tak, jakby program został uruchomiony przy użyciu polecenia run. W celu zmuszenia procesu do wznowienia działania wykorzystuje się polecenie continue.

detach - pozwala na zwolnienie procesu. Od tej chwili proces i gdb kontynuują swoje niezależne działanie.


Zabijanie procesu potomnego

kill - pozwala na zabicie procesu, który uruchamia program pod gdb.

Komenda użyteczna w przypadku odpluskwiania core dump niezależnie od działającego procesu lub w przypadku, gdy mamy zamiar ponownie skompilować program (wiele systemów nie pozwala na modyfikowanie pliku wykonywalnego w momencie jego wykonywania przez proces).


Odpluskwianie programu z wieloma procesami

W większości przypadków gdb nie zapewnia wsparcia w odpluskwianiu programów tworzących wiele wątków przy pomocy funkcji fork. Kiedy program tworzy proces potomny dla głównego procesu, gdb kontynuuje odpluskwianie głównego procesu, natomiast proces potomny wykonuje się niezależnie. 

Jeśli jednak mamy zamiar odpluskwiać proces potomny, możemy to zrobić w następujący sposób: umieszczamy odwołanie punktu kontrolnego (breakpoint) do sleep w kodzie, który wykonuje proces potomny tuż po operacji fork. Przydatne jest zdefiniowanie warunku, którego spełnienie wymusi sleep (np. kiedy zmienna środowiskowa ma określoną wartość). Dzięki temu proces będzie zawieszał swoje działanie tylko wtedy, gdy chcemy go odpluskwiać. Kolejnym krokiem jest podpięcie działającego procesu potomnego przy użyciu attach. Od tego momentu możliwe jest odpluskwianie procesu potomnego jak każdego innego procesu.

Dla niektórych systemów gdb zapewnia wsparcie dla odpluskwiania programów tworzących wiele procesów. Więcej informacji na ten temat można znaleźć w podręczniku do gdb.


Zatrzymywanie i wznawianie


Punkty kontrolne

Breakpoint pozwala na określenie miejsca w kodzie programu, po osiągnięciu którego, wykonujący się program wstrzyma działanie. Dla każdego punktu breakpoint możliwe jest zdefiniowanie wyrażenia logicznego, warunkującego wstrzymanie wykonywania programu.

Watchpoint jest specjalnym typem breakpoint-a, który pozwala na wstrzymanie działania programu w momencie, gdy zmiani się wartość zdefiniowanego wyrażenia. Kolejnym typem breakpoint-a jest catchpoint. W przypadku catchpoint-a program wstrzyma działanie, jeśli zajdzie określone zdarzenie, np. określony wyjątek w C++.

Gdb, w momencie tworzenia któregokolwiek z breakpoint-ów, przydziela mu kolejny numer, będący liczbą całkowitą. Wiele komend odnoszących się do breakpoint-ów za argument przyjmuje jego numer, a w niektórych przypadkach zakres numerów. Do podstawowych operacji na punktach kontrolnych można zaliczyć: ustawianie, aktywację, deaktywację, usunięcie.


Ustawianie  i usuwanie

break - podstawowa instrukcja, która jeśli nie dostanie argumentu, ustawia breakpoint na następną instrukcję w zaznaczonej ramce stosu.

break function; break linenum; break +offset; break -offset - niektóre rodzaje instrukcji break; kolejno: ustawienie breakpoint w miejscu wywolania funkcji function, na linię linenum; na linię rożniącą się o +/-offset od aktualnie rozpatrywanej linii instrukcji zaznaczonej ramki stosu.

(gdb) break density 

Breakpoint 1 at 0x80484ad: file przyklad1.c, line 39.

break ... if cond - warunkowe wstrzymanie wykonania programu. cond jest wyrażeniem logicznym.

watch expr - ustalenie watchpoint.

catch event - ustalenie catchpoint.

info break[n] - wyświetlenie informacji o wszsytkich zdefiniowanych breakpoint-ach.

(gdb) info break 

Num Type Disp Enb Address What 

1 breakpoint keep n 0x080484ad in density at przyklad1.c:39 

     breakpoint already hit 1 time 

2 breakpoint keep y 0x080484be in density at przyklad1.c:40

Aby usunąc breakpoint (lub zbór breakpoint-ów), wykorzystujemy polecenie clear, które może przyjmować takie same argumenty co break. Jej semantyka jest analogiczna do break, ale zamiast tworzyć breakpoint - usuwa wszystkie breakpoint-y określone czy to na funkcji lub linii kodu źródłowego itp.


Aktywowanie i deaktywowanie

Każdy breakpoint może być w jednym ze stanów:

Do deaktywowania i ponownego aktywowania używane są komendy

enable [breakpoints] [param] [range...] oraz 

disable [breakpoints] [range...]

 

(gdb) disable 1

Działają one na breakpoint-ach z zakresu range  lub na wszystkich zdefiniowanych breakpoint-ach. W celu osiągnięcia dwóch ostatnich stanów używa się parametrów: once oraz delete.


Lista poleceń dla breakpoint

Gdb daje możliwość określenie listy komand, które mają zostać wywołane w chwili, gdy program wstrzyma działanie w wyniku napotkania breakpoint-a. Służy temu polecenie:

 

command [bnum]

...command-list...

end

Np.:

(gdb) break foo if x>0

      commands

      silent

      printf "x is %d\n",x

      cont

      end

lub

(gdb) break 403

      commands

      silent

      set x = y + 4

      cont

      end


Kontynuowanie wykonania programu

Kontynuowanie oznacza wznowienie wykonywania programu do momentu pomyslnego zakończenia działania. Oprócz tego możliwe jest wykonanie kolejnego "kroku" (instrukcji), dokończenie wykonania funkcji itp.

continue [ignore-count] - kontynuuje wykonanie programu od atualnej instrukcji, ignorując ignore-count breakpoint-ów.

(gdb) continue 

Continuing. 

 

Breakpoint 2, density (x=1, y=1) at przyklad1.c:40 

40          result=result/5;

step [count] - kontynuuj działanie wykonując count linii kodu(instrukcji), następnie zwraca sterowanie do gdb. W przypadku napotkania punktu kontrolnego, wstrzymuje działanie.

(gdb) step 

40           result=result/5;

next [count] - podobnie jak step, ale nie zagłębia się w kod funkcji w chwili napotkania jej wywołania.

finish - kontynuuje działanie do chwili powrotu z funkcji. Wartość zwracana przez funkcję zostanie wyświetlona na ekranie (o ile funkcja coś zwraca).

(gdb) finish 

Run till exit from #0 eval (x=0, y=0) at przyklad1.c:31 

0x080484b8 in density (x=0, y=0) at przyklad1.c:39 

39               result=eval(x,y); 

Value returned is $2 = 625


Analiza stosu wywołań


Ramki

W momencie wykonywania programu alokowana jest struktura zwana stosem wywołań, zawierająca informację o kolejno wywoływanych funkcjach w programie. Stos składa się z ramek, które przechowują informacje o wywołanej funkcji oraz miejsce powrotu po jej zakończeniu. Ramka definiowana jest przez jej numer (nadawany przez gdb), jak i jej adres.

Ramkę można wybrać na kilka sposobów:

frame args - pozwala na przemieszczanie się między ramkami na stosie. za argument może przyjmować numer ramku lub jej adres. Drukuje informację o ramce.

select-frame - analogiczna do frame, ale nie jest drukowana informacja o ramce.

Ponadto przydatne są komendy up [n] oraz down [n].

info f - podaje szerszą informację na temat aktualnej ramki:

info args, info locals, info catch - kolejno informacja na temat argumentów, zmiennych oraz wyjątków w ramce.


Backtrace

Polecenie backtrace (wraz z wariantami) pozwala analizę tego, w jaki sposób program dotarł do miejsca, w którym zdefiniował aktualną ramkę na stosie.

(gdb) backtrace 

#0 0x080484a5 in eval (x=0, y=5) at przyklad.c:31

#1 0x0804841c in main () at przyklad.c:21


Analiza kodu źródłowego


W momencie zatrzymania działania programu, gdb spontanicznie wyświetla na ekranie linię kodu źródłowego ostatnio wykonywanego polecenia. 

Czasami zdarza się, że chcemy wprowadzić pewne modyfikacje w kod źródłowy programu w momencie jego odpluskwiania. Możemy to zrobić w standardowy sposób, używając zewnętrznego edytora lub skorzystać z możliwości gdb. Odpluskwiacz wspiera możliwość wglądu w kod źródłowy programu, jego modyfikację, a także niektóre opcje wyszukiwania. 

list - podstawowe polecenie służące do wyświetlania linii kodu źródłowego.

(gdb) list

4  #define YMAX 5 

6  int eval(int x, int y); 

7  float density(int x, int y); 

9  int main(void) { 

10     int x, y; 

11     float data[XMAX][YMAX]; 

12 

13     for (y=0; y < YMAX; y++) {


Zmiana i analizowanie danych


Wyświetlanie danych

print - podstawowe polecenie służące do przeglądania danych. print może wypisać wartość wartość prawie każdego wyrażenia, łącznie z wywołaniami funkcji skonsolidowanymi z wykonywanym programem (które są wykonywane w locie w kontekście programu). Jeżeli funkcja nie została skonsolidowana z programem, gdb uzna, że nie ma takiego symbolu w bieżącym kontekście. Do przeglądania typu danych wykorzystuje się polecenie ptype.

(gdb) print result 

$1 = 0

Można zauważyć, że po każdym poleceniu print wyświetlana wartość jest zapamiętywana do jednego z dostępnych rejestrów gdb, które są wewnętrznymi zmiennymi gdb i mogą być przydatne na dalszym etapie odpluskwiania. Możliwe jest odwoływanie się do tych zmiennych w późniejszym czasie. 

Jeżeli interesuje nas analiza pamięci, to poza nieistotnymi ograniczeniami definiowanych typów, można użyć polecenia x. x jako argument przyjmuje adres pamięci. Jeżeli zostanie podana nazwa zmiennej, wykorzysta wartość tej zmiennej jako adres. x przyjmuje także jako opcjonalny argument liczbę danych i specyfikację typu.

info registers - polecenie pozwala uzyskać informację na temat wartości rejestrów.

info variables - wyświetla listę wszystkich zmiennych w programie, uporządkowanych zgodnie z plikiem źródłowym. 


Zmiana wartości zmiennych

Zdarza się, że udało się znaleźć błąd w programie. Chcemy jednak upewnić się, czy nasze modyfikację, które mamy zamiar wprowadzić w kod programu, doprowadzą do pomyślnego zakończenia programu. Możliwe jest wykorzystanie w tym przypadku poleceń gdb umożliwiających modyfikację wartości zmiennych uruchomionego programu, wznowienie wykonania programu od innego miejsca, wysłanie sygnału do programu lub ponowne uruchomienie programu.

print varname=newval - polecenie pozwala na przypisanie wartości newval zmiennej varname, a następnie wyświetleniu nowej wartości zmiennej. Jeżeli nie jesteśmy zainteresowani wyświetlaniem wartości zmiennej można użyć polecenia set var. W przypadku zmiany wartości zmiennej pomocne staje się polecenie whatis, które jako parametr przyjmuje identyfikator, a wyświetla informację na temat podanego identyfikatora.

Gdb zapewnia wsparcie dla lokalnych zmiennych odpluskwiacza (wspomniane już przy okazji print). Nazwy tych zmiennych rozpoczynają się od '$' i podlegają tym samym prawom, co zmienne programu, tzn. można drukować ich wartość, zmieniać itp. Zmienne gdb nie mają wpływu na działający program.


Analiza zrzutu pamięci


Plik zrzutu pamięci (ang. core dump) jest po prostu zrzutem obrazu pamięci procesu z chwili awarii. Możliwe jest wykorzystanie tego pliku przez gdb do analizy stanu programu (czyli odczytania takich rzeczy jak wartości zmiennych i dane) i ustalenia powodu awarii. Do najczęstszych powodów awarii należą:

W Linuksie pliki zrzutu pamięci nazywane są core i umieszczane są w bieżącym katalogu roboczym działającego procesu (zdarza się, że powłoka domyślnie nie zapisuje plików core; aby włączyć ich zapisywanie, należy użyć polecenia: ulimit -c unlimited). Aby plik zrzutu był użyteczny, program musi być skompilowany z włączonym kodem debugowania, jak opisano wcześniej.

Aby gdb działał z plikiem zrzutu, należy określić nie tylko nazwę pliku zrzutu pamięci, ale także nazwę pliku wykonywalnego, który go wygenerował (sam plik zrzutu pamięci nie zawiera żadnych informacji potrzebnych do debugowania).

Kiedy pomyślnie uruchomimy gdb na pliku zrzutu pamięci, możemy przystąpić do analizy pliku wykorzystując większość z omówionych wcześniej komend debugera. 


Używanie Emacsa z gdb


Emacs jest wyposażony w trub debugowania, który uruchamia gdb (lub inny odpluskwiacz) w zintegrowanym środowisku śledzenia programu udostępnianym przez Emacs.

Aby uruchomić gdb w Emacs, należy użyć polecenia Emacs M-x gdb i jako argument podać nazwę pliku wykonywalnego do debugowania. W oknie debugowania można stosować kilka specjalnych kombinacji klawiszy, jednak są one stosunkowo długie, przez co mogą okazać się niewygodne w użyciu.

Emacs łatwo jest przystosować do własnych potrzeb i istnieje wiele rozszerzeń do tego interfejsu gdb, które można samemu napisać. Możliwe jest zdefiniowanie klawiszy Emacsa do innych, powszechnie używanych poleceń gdb.


Przykłady użycia gdb


Poniżej zamieszczono dwa przykłady użycia gdb.

Przykład 1

Program przerywa swoje wykonanie z błędem. Analizie podlega plik core.

Przykład 2

Program wykonuje się poprawnie, tzn. kończy działanie. Problem polega na tym, że otrzymane wyniki są błędne. Analizie podlega program wykonywalny.


Bibliografia



autor: Paweł Kaczan