Strona główna.
Następny rozdział: Opcje odpluskwiania w gcc. Formaty plików obiektowych i przygotowanych do odpluskwiania.

Podstawy debugowania

Spis treści


Podstawy debugowania

Legenda głosi, że Seymour Cray zakodował kiedyś cały system operacyjny dla jednego ze swoich komputerów — od razu bezbłędnie — wprowadzając go za pomocą przełączników na obudowie maszyny. Jeśli jest na sali ktoś o podobnych umiejętnościach, to może teraz przestać słuchać ;-)

Wszystkich pozostałych zapraszamy na prezentację o metodach debugowania. Omówimy sposoby tropienia błędów w programach użytkownika i w kodzie jądra, od tych najprostszych po bardziej wyrafinowane.

Powrót do góry strony


"D... debugging"

Wstawiamy w podejrzanych miejscach programu wywołania printf (lub czegoś podobnego), po czym patrzymy, które się jeszcze wykonają, a które już nie. W wersji bardziej zaawansowanej wypisujemy także wartości wybranych zmiennych.

Przykład

 if (sscanf(operation, "%ld %c %ld", &args[0], &op, &args[1]) != 3)
         return -EINVAL;                 /* Niepoprawne wyrazenie */
     printk(KERN_DEBUG "calc: Skladnia wyrazenia poprawna...\n");

     for (i=0; i<2; i++) {
         if (ref & (1<<i)) {
             if ((args[i] < mem) && (args[i] >= 0)) {
                 cmc = cells[args[i]]->data;
                 spin_lock(&cmc->lock);
                 printk(KERN_DEBUG "calc: Pobieramy zawartosc komorki %d\n", i);
                 if (sscanf(cmc->value, "%ld", &args[i]) !=1) {
                     spin_unlock(&cmc->lock);
                     return -EFAULT;
                 }
                 spin_unlock(&cmc->lock);
             } else return -EFAULT;

Powrót do góry strony


Kiedy to nie wystarcza...

Możemy chcieć:

Jednym słowem, potrzebujemy debuggera

Powrót do góry strony


GNU Debugger (gdb)

Potrafi wszystko to, a nawet więcej ;-) Nowsze wersje radzą sobie z programami wielowątkowymi czy dynamicznie ładowanym kodem. Można używać gdb bezpośrednio z linii komend, ale istnieje też mnóstwo graficznych nakładek. Do popularniejszych należą:

Sam gdb dostępny jest pod adresem www.gnu.org/software/gdb/

Powrót do góry strony


Jak się tego używa?

Najpierw musimy skompilować nasz program z odpowiednimi opcjami (-g lub --debug).
Gdy już to zrobimy, możemy uruchomić go pod kontrolą debuggera:

 % gdb nasz_program

... zdefiniować miejsca, w których chcemy wstrzymać wykonanie:

 (gdb) break funkcja

... i jazda!

 (gdb) run

Powrót do góry strony


Jak się tego używa, c.d.

Po zatrzymaniu programu możemy się trochę rozejrzeć:

Polecenie print $ wyświetli wartość ostatnio oglądanego wyrażenia,
można go użyć np. do przeglądania listy:

 (gdb) print *glowa_listy
 (gdb) print *$.nast
 (gdb) ...
Podobnie print $n pokaże n-te ostatnio oglądane wyrażenie

Powrót do góry strony


Jak się tego używa, c.d. c.d.

Gdy już wiemy co i jak, sprawdźmy nasze domysły:

Mamy też oczywiście możliwość śledzenia przebiegu programu krok po kroku:

W razie zmiany kodu i rekompilacji można program zakończyć i uruchomić ponownie bez wychodzenia z gdb: najpierw wydajemy polecenie kill, a potem run

Powrót do góry strony


Zapis typowej sesji z gdb

 [akmac@localhost ~]$ gdb calc
 GNU gdb Red Hat Linux (6.1post-1.20040607.41rh)
 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/tls/libthread_db.so.1".
 
 (gdb) l
 7           struct stackelt* nast;
 8       };
 9
 10      void poloz(struct stackelt**, int);
 11      int dodawaj(struct stackelt**);
 12      int mnoz(struct stackelt**);
 13
 14      int main()
 15      {
 16          struct stackelt* stos = NULL;
 (gdb) b dodawaj
 Breakpoint 1 at 0x8048606: file calc.c, line 52.
 (gdb) r
 Starting program: /home/akmac/calc
 Reading symbols from shared object read from target memory...done.
 Loaded system supplied DSO at 0xb7f25000
 12
 13
 14
 +
 
 Breakpoint 1, dodawaj (stos=0xbfb24c9c) at calc.c:52
 52          int wynik = 0;
 (gdb) n
 54          while (*stos != NULL) {
 (gdb) info local
 elem = (struct stackelt *) 0x4604f8
 wynik = 0
 (gdb) info args
 stos = (struct stackelt **) 0xbfb24c9c
 (gdb) p **stos
 $1 = {wartosc = 14, nast = 0x8525018}
 (gdb) p *$.nast
 $2 = {wartosc = 13, nast = 0x8525008}
 (gdb)
 $3 = {wartosc = 12, nast = 0x0}
 (gdb) display wynik
 1: wynik = 0
 (gdb) n
 55              elem = *stos;
 1: wynik = 0
 (gdb) set wynik=10
 1: wynik = 10
 (gdb) n
 56              *stos = (*stos)->nast;
 1: wynik = 10
 (gdb)
 57              wynik += elem->wartosc;
 1: wynik = 10
 (gdb)
 58              free(elem);
 1: wynik = 24
 (gdb) undisplay 1
 (gdb) watch *stos
 Hardware watchpoint 2: *stos
 (gdb) c
 Continuing.
 Hardware watchpoint 2: *stos
 
 Old value = (struct stackelt *) 0x8525018
 New value = (struct stackelt *) 0x8525008
 dodawaj (stos=0xbfb24c9c) at calc.c:57
 57              wynik += elem->wartosc;
 (gdb) info break
 Num Type           Disp Enb Address    What
 1   breakpoint     keep y   0x08048606 in dodawaj at calc.c:52
         breakpoint already hit 1 time
 2   hw watchpoint  keep y              *stos
         breakpoint already hit 1 time
 (gdb) c
 Continuing.
 Hardware watchpoint 2: *stos
 
 Old value = (struct stackelt *) 0x8525008
 New value = (struct stackelt *) 0x0
 dodawaj (stos=0xbfb24c9c) at calc.c:57
 57              wynik += elem->wartosc;
 (gdb) n
 58              free(elem);
 (gdb) n
 54          while (*stos != NULL) {
 (gdb) n
 60          printf("Suma: %d\n", wynik);
 (gdb) n
 Suma: 49
 61          poloz(stos, wynik);
 (gdb) s
 poloz (gdzie=0xbfb24c9c, co=49) at calc.c:41
 41          elem = malloc(sizeof(struct stackelt));
 (gdb) s
 42          if (elem == NULL)
 (gdb) s
 44          elem->nast = *gdzie;
 (gdb) where
 #0  poloz (gdzie=0xbfb24c9c, co=49) at calc.c:44
 #1  0x08048665 in dodawaj (stos=0xbfb24c9c) at calc.c:61
 #2  0x08048557 in main () at calc.c:26
 (gdb) finish
 Run till exit from #0  poloz (gdzie=0xbfb24c9c, co=49) at calc.c:44
 Hardware watchpoint 2: *stos

 Old value = (struct stackelt *) 0x0
 New value = (struct stackelt *) 0x8525008
 poloz (gdzie=0xbfb24c9c, co=49) at calc.c:47
 47      }
 (gdb) c
 Continuing.
 
 Watchpoint 2 deleted because the program has left the block in
 which its expression is valid.
 0x08048557 in main () at calc.c:26
 26                  dodawaj(&stos);
 (gdb) c
 Continuing.
 15
 ^C
 Program received signal SIGINT, Interrupt.
 0xb7f25402 in __kernel_vsyscall ()
 (gdb) dis 1
 (gdb) c
 Continuing.
 +
 Suma: 64

Powrót do góry strony


Gdzie się można zatrzymać?

W gdb istnieją trzy rodzaje pułapek mogących spowodować wstrzymanie wykonania programu:

Pułapki mogą być warunkowe (break funkcja if warunek) albo tymczasowe (tbreak funkcja)

Można je usuwać (clear plik.c:nr_linii, clear funkcja, delete [nr_pulapki])
albo wyłączać i włączać (disable nr_pulapki / enable nr_pulapki).

Numery pułapek i inne informacje o nich zobaczymy wydając polecenie info breakpoints

Powrót do góry strony


Co jeszcze można zrobić

Jeśli program spowodował błąd i zakończył się tworząc na dysku zrzut pamięci (plik core), uruchomienie debuggera poleceniem gdb program plik_core pozwoli nam przeanalizować przyczynę katastrofy.

Historię (stos) wywołań funkcji zobaczymy wydając polecenie backtrace. Możemy się po nim poruszać w górę i w dół poleceniami prevnext, albo od razu przeskoczyć do ramki o wybranym numerze (poleceniem frame nr_ramki) i śledzić przebieg programu od tego miejsca.

Dalej postępujemy jak zwykle: badamy i modyfikujemy wartości zmiennych, wykonujemy program instrukcja po instrukcji, itp.


GNU debugger potrafi także analizować już uruchomione programy: gdb program pid
czy nawet przełączać się między procesami: (gdb) detach, (gdb) attach pid

Powrót do góry strony


Sesja z gdb uruchomionym na pliku core

 [akmac@localhost ~]$ ./calc
 12
 13
 +
 Suma: 25
 10
 *
 Segmentation fault (core dumped)
 [akmac@localhost ~]$ gdb calc core.14931
 GNU gdb Red Hat Linux (6.1post-1.20040607.41rh)
 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/tls/libthread_db.so.1".
 
 Reading symbols from shared object read from target memory...done.
 Loaded system supplied DSO at 0xb7f04000
 Core was generated by `./calc'.
 Program terminated with signal 11, Segmentation fault.
 
 Reading symbols from /lib/tls/libc.so.6...done.
 Loaded symbols for /lib/tls/libc.so.6
 Reading symbols from /lib/ld-linux.so.2...done.
 Loaded symbols for /lib/ld-linux.so.2
 #0  0x08048692 in mnoz (stos=0xbfa01c2c) at calc.c:72
 72              *stos = (*stos)->nast;
 (gdb) bt
 #0  0x08048692 in mnoz (stos=0xbfa01c2c) at calc.c:72
 #1  0x08048580 in main () at calc.c:28
 (gdb) info local
 elem = (struct stackelt *) 0x0
 wynik = 250
 (gdb) up
 #1  0x08048580 in main () at calc.c:28
 28                  mnoz(&stos);
 (gdb) info local
 stos = (struct stackelt *) 0x0
 polecenie = "*\n\000\000\000\000\000\0008\034"
 liczba = 10
 ile = -1080026088
 operacja = 8 '\b'
 (gdb) frame 0
 #0  0x08048692 in mnoz (stos=0xbfa01c2c) at calc.c:72
 72              *stos = (*stos)->nast;
 (...)

Powrót do góry strony


Podczepianie gdb do działającego procesu

 [akmac@localhost ~]$ echo $$
 14940
 [akmac@localhost ~]$ exec ./calc
 12
 13
 +
 Suma: 25
... tymczasem w drugim terminalu:
 [akmac@localhost ~]$ gdb
 GNU gdb Red Hat Linux (6.1post-1.20040607.41rh)
 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".
 (gdb) att 14940
 Attaching to process 14940
 warning: The current VSYSCALL page code requires an existing execuitable.
 Use "add-symbol-file-from-memory" to load the VSYSCALL page by hand
 Reading symbols from /home/akmac/calc...done.
 Using host libthread_db library "/lib/tls/libthread_db.so.1".
 Reading symbols from /lib/tls/libc.so.6...done.
 Loaded symbols for /lib/tls/libc.so.6
 Reading symbols from /lib/ld-linux.so.2...done.
 Loaded symbols for /lib/ld-linux.so.2
 0xb7fdd402 in ?? ()
 (gdb) bt
 #0  0xb7fdd402 in ?? ()
 #1  0x00ce0253 in __read_nocancel () from /lib/tls/libc.so.6
 #2  0x00c85aa8 in _IO_file_read_internal () from /lib/tls/libc.so.6
 #3  0x00c8482e in _IO_new_file_underflow () from /lib/tls/libc.so.6
 #4  0x00c86e0b in _IO_default_uflow_internal () from /lib/tls/libc.so.6
 #5  0x00c86bfd in __uflow () from /lib/tls/libc.so.6
 #6  0x00c7bc40 in _IO_getline_info_internal () from /lib/tls/libc.so.6
 #7  0x00c7bb7f in _IO_getline_internal () from /lib/tls/libc.so.6
 #8  0x00c7aaa9 in fgets () from /lib/tls/libc.so.6
 #9  0x080484ff in main () at calc.c:22
 (gdb) b poloz
 Breakpoint 1 at 0x80485c3: file calc.c, line 41.
 (gdb) c
 Continuing.
 
 Breakpoint 1, poloz (gdzie=0xbffdd79c, co=12) at calc.c:41
 41          elem = malloc(sizeof(struct stackelt));
 (gdb) c
 Continuing.
 
 Breakpoint 1, poloz (gdzie=0xbffdd79c, co=13) at calc.c:41
 41          elem = malloc(sizeof(struct stackelt));
 (gdb) c
 Continuing.
 
 Breakpoint 1, poloz (gdzie=0xbffdd79c, co=25) at calc.c:41
 41          elem = malloc(sizeof(struct stackelt));
 (gdb) det
 Detaching from program: /home/akmac/calc, process 14940
 (gdb) q
 [akmac@localhost ~]$

Powrót do góry strony


Skąd wziąć plik core?

Można też zdefiniować wzorzec, według jakiego mają być nazywane powstające zrzuty pamięci, pisząc do pliku /proc/sys/kernel/core_pattern. Więcej informacji: man bashman proc.

Powrót do góry strony


Jak działa debugger?

Skompilowanie programu z opcją --debug powoduje, że do pliku wykonywalnego dodawane są informacje o symbolach (pozwalające powiązać adresy w pamięci z nazwami znajdujących się tam zmiennych i funkcji), a także kod źródłowy programu wraz z numerami poszczególnych linii. Debugger może kontrolować przebieg programu przy użyciu wywołania systemowego ptrace.

Wygląda to zwykle tak, że debugger wykonuje fork, po czym proces potomny wywołuje ptrace z flagą PTRACE_TRACEME, a następnie exec i zaczyna wykonywać kod programu, który chcemy śledzić. Chcąc podłączyć się do istniejącego procesu, debugger używa wywołania ptrace z flagą PTRACE_ATTACH, podając przy tym pid procesu, który ma być śledzony — proces ten zostaje wówczas "adoptowany" przez debugger (wskaźnik p_pptr śledzonego procesu zostaje ustawiony na strukturę task_struct procesu debuggera).

Niezależnie od tego, która z powyższych metod została użyta, samo debugowanie procesu przebiega tak samo. Wysłanie jakiegokolwiek sygnału (oprócz SIGKILL) śledzonemu procesowi powoduje jego wstrzymanie, o czym jego rodzic (czyli debugger) dowiaduje za pośrednictwem funkcji systemowej wait. Rodzic może teraz, wywołując ptrace z odpowiednią flagą:

Dokładniejsze omówienie ptrace można znaleźć w scenariuszu do 13 zajęć laboratoryjnych oraz w podręczniku systemowym (man ptrace). Więcej na temat informacji, które mogą zostać zaszyte w pliku wykonywalnym — w następnym rozdziale.

Powrót do góry strony


© Adam Maciejewski