Aby zacząć używać GDB (GNU Project Debugger) powinniśmy skompilować nasz kod z opcją -ggdb, co powoduje dodanie symboli dla debuggera. Podczas debuggowania nie używamy żadnych opcji optymalizacji kodu, które potencjalnie mogą mocno zmodyfikować sposób i kolejność wykonywania instrukcji.
Na początek podstawowe komendy gdb i sposób uruchamiania procesów via gdb:
0 1 2 3 4 5 6 |
$ gdb program #debuguj "program" (gdb) run -v #uruchamia załadowany program z parametrami (gdb) bt #backtrace (w wypadku błędu programu) (gdb) info registers #zrzut rejestrów (gdb) disass #dissasembler na pliku |
Jako dwóch przykładów użyję kodu example1/example2 z artykułu o strace (który bardzo polecam - nie zawsze mamy możliwość komfortowej kompilacji i pracy z GDB, a często strace może okazać się wystarczający w diagnozowaniu problemu).
W przypadku example1.c nie zaobserwujemy żadnych problemów, program wykona się poprawnie.
0 1 2 3 4 5 6 7 8 9 |
odcinek@ix:~/GDB # cat example1.c #include <stdio.h> #include <stdlib.h> int main(void) { printf(“Hello worldn”); exit(0); } |
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
odcinek@ix:~/GDB # gcc example1.c -o example1 -ggdb odcinek@ix:~/GDB # ./example1 Hello world odcinek@ix:~/GDB # gdb ./example1 GNU gdb 6.8 Copyright (C) 2008 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-pc-linux-gnu"... (gdb) run Starting program: /home/odcinek/GDB/example1 Hello world Program exited normally. (gdb) bt No stack. (gdb) info registers The program has no registers now. (gdb) quit |
Na przykładzie kodu example2.c przetestujemy szukanie błędu w programie. Program próbuje połączyć się na localhost:5000 i wysłać tam dane (*send_data):
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> int main(void) { int sock; char *send_data = "DANE DANE DANEn"; struct hostent *host; struct sockaddr_in server_addr; host = gethostbyname("localhost"); sock = socket(AF_INET, SOCK_STREAM, 0); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(5000); server_addr.sin_addr = *((struct in_addr *)host->h_addr); bzero(&(server_addr.sin_zero),8); connect(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)); printf("SEND DATAn"); send(sock,send_data,strlen(send_data), 0); close(sock); return 0; } |
Po uruchomieniu example2 nie zwraca żadnego błędu. Ciekawy jest natomiast kod wyjścia z programu:
0 1 2 3 4 5 |
odcinek@ix:~/GDB # ./example2 SEND DATA odcinek@ix:~/GDB # echo $? 141 |
Jako kod wyjścia dostajemy 141, zamiast 0 (które oznacza prawidłowe zakończenie procesu). Pójdźmy zatem dalej, kompilując example2.c z opcją -ggdb i śledząc przebieg wykonywania:
0 1 2 3 4 5 6 7 8 9 10 11 |
(gdb) run Starting program: /home/odcinek/GDB/example2 SEND DATA Program received signal SIGPIPE, Broken pipe. 0xb7868424 in __kernel_vsyscall () (gdb) bt #0 0xb7868424 in __kernel_vsyscall () #1 0xb77efd51 in send () from /lib/libc.so.6 #2 0x08048662 in main () at example2.c:33 |
Okazuje się że w linii 33 (example2.c:33) dostajemy SIGPIPE, czyli sygnał próby zapisu do potoku, do którego pisać się nie da (w tym wypadku chodzi o niepowodzenie funkcji connect z linii 30). Wniosek: connect powinien zostać obłożony warunkiem, na przykład tak:
0 1 2 3 4 5 |
if(connect(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))!=0){ perror("connect failed"); return 1; } |
Na koniec należy dodać, że nigdy nie powinno się korzystać z kodu skompilowanego z opcjami debuggowania w środowisku docelowym, produkcyjnym - może to wpłynąć negatywnie na wydajność aplikacji. Dodatkowo można go strippować (komenda strip), usuwając wszystkie zbędne przy wykonywaniu informacje - co zmniejsza rozmiar pliku wykonywalnego i teoretycznie może poprawić szybkość wykonywania kodu.