Strona główna. |
Następny rozdział: Opcje odpluskwiania w gcc. Formaty plików obiektowych i przygotowanych do odpluskwiania. |
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.
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.
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;
Możemy chcieć:
Jednym słowem, potrzebujemy debuggera
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/
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
Po zatrzymaniu programu możemy się trochę rozejrzeć:
Polecenie print $ wyświetli wartość ostatnio oglądanego
wyrażenia,
można go użyć
(gdb) print *glowa_listy (gdb) print *$.nast (gdb) ...
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
[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
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
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 prev i next, 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
[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; (...)
[akmac@localhost ~]$ echo $$ 14940 [akmac@localhost ~]$ exec ./calc 12 13 + Suma: 25
[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 ~]$
% ulimit -c 50000
Można też zdefiniować wzorzec, według jakiego mają być nazywane powstające
zrzuty pamięci, pisząc
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.
© Adam Maciejewski |