GDB 사용하기 에서 GDB 의 기본적인 사용법에 대해서 익혔다면, 여기서는 기본적인 것을 토대로 여러가지 환경에서의 디버깅에 대해서 다룰 것이다.
코어파일을 위한 디버깅
코어파일(core)은 프로세스에 예외가 발생하여 중지되는 순간의 프로세스 이미지를 의미한다. 앞의 문서 GDB 사용하기 에서 예로 들었던 bug.c 를 컴파일하여 실행하면, 코어파일이 생성된다.
만일 생성되지 않는다면, 쉘에서 코어 파일의 크기가 0 으로 설정되어 있는 경우이기 때문에
#ulimit -S -c 10000000
위와 같이 명령을 내려서 파일의 제한 크기를 늘리면 된다. 이제 다시 실행해보자.
코어 파일이 생성되었을 것이다. 나의 경우, 'core.3108' 라는 파일이 생성되었다. 뒤에 붙은 숫자는 PID 를 의미한다.
이렇게 만들어진 코어파일을 이용하여 디버깅을 하기 위해서는 다음과 같이 실행하면 된다.
#gdb a.out core.3108
이 상태에서 'bt' 명령을 이용해서 디버깅 하면 된다.
실행 중인 프로세스 디버깅
실행 중인 프로세스의 디버깅의 가장 흔한 경우는 무한 루프에 빠졌을 때이다.
예를 들어 어떤 프로그램의 실행이 무한 루프로 인해 실행되고 있을 때, 이 프로세스의 PID 값을 알아내서 GDB 를 이용해서 디버깅 할 수 있다.
여기서는 무한 루프 프로세스의 PID 가 3217 이라고 한다면,
#gdb a.out (gdb) attach 3217
이런식으로 하면, 무한 루프에서 프로세스가 중지되고 디버깅 가능한 상태가 된다.
문제의 원인을 찾았으면,
(gdb) detach
명령을 통해 프로세스를 놓아야 한다.
멀티프로세스 프로그램 디버깅
이 경우에는 GDB-5.3 버전에서는 디버깅할 수 없기 때문에, 앞에서 설명했듯이 6.2 로 업데이트를 해야 한다.
여기서 사용할 프로그램은 다음과 같다.
#include <stdio.h> #include <unistd.h> int main() { int pid, var, i = 0x3fffffff; pid = fork(); if(pid) printf("%d\n", pid); while(i--); if(!pid){ for (i=0; i<10000; i++){ var += i; sleep(1); } } for(i=0; i<10000; i++){ var -= i; sleep(1); } return 0; }
작성후에 컴파일을 한다. 여기서 터미널 세션을 하나 더 만든다. 그리고 컴파일한 프로그램을 실행시킨다.
#./a.out 32818
새로 만든 터미널 세션에서 다음을 입력한다.
#gdb a.out 32818
소스 프로그램의 특정상 0x3ffffffff 이 0 이 되지 전까지 부분이 delay 를 주는 부분이다. 이 시간안에 새로운 터미널 세션에서 GDB 를 실행시켜야 한다. 만일 그렇지 않으면,
Attaching to program: /root/a.out, process 28303 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 0xffffe002 in ?? ()
이런식으로 메세지가 출력될 것이다. 이 상황은 정상적으로 디버깅할 수 없는 상황이므로 처음부터 다시 프로그램을 실행시켜야 한다.
Attaching to program: /root/a.out, process 28324 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 0x080483d2 in main () at fork.c:11 11 while(i--);
위와 같은 메세지가 출력되었다면, 제대로 실행시킨 것이다.
(gdb) b 14 (gdb) b 15 /* 지연 루프인 while(i--) 바로 뒤에 breakpoint 를 설정 (gdb) c
이제부터는 일반적인 프로세스 디버깅과 동일하다.
멀티스레드 프로그램 디버깅
여기서 사용할 프로그램은 다음과 같다.
#include <stdio.h> #include <pthread.h> int var; void *add(void *n){ while(1){ var++; /* add 스레드에 breakpoint 설정을 해야 함*/ printf("add thread!\n"); } } void *sub(void *n){ while(1){ var--; /* sub 스레드에 breakpoint 설정을 해야함*/ printf("sub thread!\n"); } } int main(){ pthread_t p_thread[2]; pthread_create(&p_thread[0], NULL, add, NULL); pthread_create(&p_thread[1], NULL, sub, NULL); pthread_join(p_thread[0], NULL); /* 스레드 종료 */ pthread_join(p_thread[1], NULL); /* 스레드 종료 */ return 0; }
간략히 프로그램 설명을 하자면, 수행도중에 add, sub 스레드를 생성한다. 무한 루프가 되기때문에 두 스레드가 번갈아 가면서 화면에 출력한다.
#gcc -o thread thread.c -lpthread -g
다음과 같이 컴파일한다. 실행파일과 함께 GDB 를 실행한다.
#gdb thread
스레드 종료 함수인 pthread_join() 앞에 breakpoint 를 잡는다.
(gdb) b 26 (gdb) info threads 3 Thread 1090694208 (LWP 28440) 0xffffe002 in ?? () 2 Thread 1082305728 (LWP 28439) 0xffffe002 in ?? () * 1 Thread 1073914496 (LWP 28432) main () at thread.c:26
총 3 개의 스레드를 볼 수 있다. 나머지 add 와 sub 스레드는 아직 실행되지 않았기 때문에 '??' 로 나왔다.
(gdb) b 8 thread 2 (gdb) b 15 thread 3 (gdb) c
각 스레드의 진입에 breakpoint 를 설정했기 때문에, 차례대로 add 태스크와 sub 태스크에 각각 breakpoint 가 걸려서 멈춘다.
(gdb) c (gdb) info threads 3 Thread 1090694208 (LWP 28488) sub (n=0x0) at thread.c:17 * 2 Thread 1082305728 (LWP 28487) add (n=0x0) at thread.c:9 1 Thread 1073914496 (LWP 28480) 0xffffe002 in ?? () (gdb) c (gdb) info threads 3 Thread 1090694208 (LWP 28488) sub (n=0x0) at thread.c:17 2 Thread 1082305728 (LWP 28487) add (n=0x0) at thread.c:9 * 1 Thread 1073914496 (LWP 28480) 0xffffe002 in ?? ()
※ 각 스레드의 지역 변수의 경우, 예를 들어 add 스레드에 있는 지역변수를 현재 sub 스레드에 디버깅 중에는 변수 값을 출력할 수 없다.
보고 싶다면, 보기를 원하는 스레드로 옮긴 후 봐야 한다.
(gdb) thread [N]
여기서는
(gdb) thread 2 또는 thread 3
할 수 있다.
프로그램 디버깅 중에 시그널 보내기
프로그램이 수행도중 특정 시그널이 발생하게 되면 등록된 시그널 핸들러 함수가 실행된다. gdb 를 이용해서 디버깅 중에 강제적(?)으로 시그널을 발생시켜야 할 경우가 있다.
가장 흔한 시그널 중에 하나인 SIGINT 의 경우, 'Ctrl + C' 키의 조합으로 발생가능하다. 하지만, gdb 에서 이와 같은 방법으로 시그널 발생시 gdb 자체적으로 디버깅을 종료할 것인지를 묻는다.
이것은 명백히 우리가 원하는 오퍼레이션이 아니다. 아래의 예제 코드를 보자!
void mz_my_signal(int s_signal) { switch(s_signal) { case SIGINT: g_mz_break = 1; (void)signal(s_signal, mz_my_signal); break; default: (void)fprintf(stdout, "unknown signal ! (%d)\n", s_signal); break; } } ... int main() { ... (void)signal(SIGINT, mz_my_signal); ... }
시그널이 발생해야 하는 지점에서 아래와 같이 입력한다.
(gdb) signal SIGINT
또한 'info signals' 명령어를 실행하면, 발생시킬 수 있는 시그널의 종류를 볼 수 있다.
Signal Stop Print Pass to program Description SIGHUP Yes Yes Yes Hangup SIGINT Yes Yes No Interrupt SIGQUIT Yes Yes Yes Quit SIGILL Yes Yes Yes Illegal instruction SIGTRAP Yes Yes No Trace/breakpoint trap SIGABRT Yes Yes Yes Aborted SIGEMT Yes Yes Yes Emulation trap SIGFPE Yes Yes Yes Arithmetic exception SIGKILL Yes Yes Yes Killed SIGBUS Yes Yes Yes Bus error SIGSEGV Yes Yes Yes Segmentation fault SIGSYS Yes Yes Yes Bad system call SIGPIPE Yes Yes Yes Broken pipe SIGALRM No No Yes Alarm clock SIGTERM Yes Yes Yes Terminated SIGURG No No Yes Urgent I/O condition SIGSTOP Yes Yes Yes Stopped (signal) SIGTSTP Yes Yes Yes Stopped (user) SIGCONT Yes Yes Yes Continued SIGCHLD No No Yes Child status changed SIGTTIN Yes Yes Yes Stopped (tty input) SIGTTOU Yes Yes Yes Stopped (tty output) SIGIO No No Yes I/O possible SIGXCPU Yes Yes Yes CPU time limit exceeded SIGXFSZ Yes Yes Yes File size limit exceeded SIGVTALRM No No Yes Virtual timer expired SIGPROF No No Yes Profiling timer expired SIGWINCH No No Yes Window size changed SIGLOST Yes Yes Yes Resource lost SIGUSR1 Yes Yes Yes User defined signal 1 SIGUSR2 Yes Yes Yes User defined signal 2 SIGPWR Yes Yes Yes Power fail/restart SIGPOLL No No Yes Pollable event occurred SIGWIND Yes Yes Yes SIGWIND SIGPHONE Yes Yes Yes SIGPHONE SIGWAITING No No Yes Process's LWPs are blocked SIGLWP No No Yes Signal LWP ...