본문 바로가기

프로그램언어/C++

GDB를 이용한 Linux 소프트웨어의 디버깅

David Seager, CICS/390 Development, IBM Hursley

요약: Linux의 특징은 GNU 디버거(debugger) 또는 쉘의 gdb이다. gdb 사용으로 프로그램의 내부 구조 이해, 변수 값의 인쇄 및 소스 코드를 통한 정지점과 싱글 스텝을 설정할 수 있다. gdb는 프로그램 코드 상의 문제를 해결할 수 있는 강력한 무기이다. 이 글에서 gdb가 얼마나 멋지고 유용한지를 설명한다.


컴파일 하기

디버그하기 전에, 디버그 하고자 하는 프로그램에 디버깅 정보를 컴파일 한다. 그럼으로써 gdb가 사용했던 변수, 라인 및 함수를 실행할 수 있다. gcc(또는 g++)에서 '-g' 옵션을 이용하여 프로그램을 컴파일 한다. :

gcc -g eg.c -o eg

gdb 실행하기

gdb는 프로그램명이 매개 변수(예를 들어, 'gdb eg') 쉘에서 'gdb' 명령으로 실행된다. gdb에서 파일 명령으로 디버깅 용 프로그램을 로드 할 수 있다(예, 'file eg'). 프로그램과 동일한 디렉토리에서 명령이 실행되어, 일단 로드 되면 gdb command 'run' 으로 프로그램이 시작한다.

디버깅 세션의 예제

gdb가 정상적인 실행을 하여 완료 시점까지 문제가 발생하지 않으면 프로그램은 계속 실행된다. 만일 에러가 발생한다면? 이 같은 경우, gdb가 프로그램을 제어하고 중지시킬 것이다. 따라서 여러분이 문제의 원인을 파악할 수 있다. 이러한 시나리오를 유발하기 위해서 예제 프로그램을 사용할 것이다. :


#include <stdio.h>

int wib(int no1, int no2)
{
  int result, diff;
  diff = no1 - no2;
  result = no1 / diff;
  return result;
}

int main(int argc, char *argv[])
{
  int value, div, result, i, total;

  value = 10;
  div = 6;
  total = 0;

  for(i = 0; i < 10; i++)
  {
    result = wib(value, div);
    total += result;
    div++;
    value--;
  }

  printf("%d wibed by %d equals %d\n", value, div, total);
  return 0;
}

이 프로그램은 for 루프(loop)를 10회 정도 실행한다. 또 'wib() 함수를 사용해서 누적 값을 계산하고 결과를 프린트한다.

사용자가 가장 선호하는 동일 행 스페이싱을 포함한 text editor를 입력하여 'eg1.c'를 다른 이름으로 저장한 후 'gcc-g eg1.c-eg1"로 컴파일하고 'gdb eg1'로 gdb를 실행한다. 'run'으로 프로그램을 실행하면 다음과 같은 메시지가 나타난다.:


Program received signal SIGFPE, Arithmetic exception.
0x80483ea in wib (no1=8, no2=8) at eg1.c:7
7         result = no1 / diff;
(gdb) 

Gdb는 프로그램이 7 행에서 arithmetic exception을 얻고 행과 argument 값을 wib() 함수에 프린트한다. 7 행에서 소스 코드를 보려면 'list'를 명령한다. 이 명령은 보통 10개의 행을 프린트한다. 다시 'list'를 입력하면 (또는 최종 명령을 반복시키는 리턴 키를 누른다.) 프로그램의 다음 10개 행이 열거된다. gdb 메시지에서 프로그램이 변수 "no1"에서 "diff"로 분할하는 7 행에서 분할이 잘못되었음을 나타낸다..

gdb의 'print' 명령을 변수명과 같이 사용하면 변수 값을 알 수 있다. 'print no 1'과 'print off'를 입력하면 "no1" 및 "diff"이 무엇인지 알 수 있다. 결과는 다음과 같다.:


(gdb) print no1
$5 = 8
(gdb) print diff
$2 = 0

Gdb는 "no1"은 8 이고, "diff"는 4임을 나타낸다. 이 값과 7 행을 통해 arithmetic exception은 제로에 의한 나눗셈이 원인임을 알 수 있다. Listing은 변수 "diff"이 6 행에서 계산됨을 나타낸다. "diff" expression을 보충해서 'print no1 - no2'를 입력하면 값을 다시 구할 수 있다. Gdb는 wib 함수의 argument 둘 모두 8이어서 이것이 언제 발생하는지 알기 위하여 wib()를 호출하는 main() 함수를 점검해야 한다는 것을 나타낸다. 잠시 프로그램을 멈추고(die) 'continue' 명령으로 gdb를 실행한다.:


(gdb) continue
Continuing.

Program terminated with signal SIGFPE, Arithmetic exception.
The program no longer exists.

정지점(breakpoints) 사용

main()에서 무엇이 진행되는지를 검토하기 위해 특정 행(particular line)이나 프로그램 코드함수에 정지점을 설정할 수 있으므로 gdb가 여기에 이를 때 실행을 막을 것이다. 명령어 'break main'로 main() function을 입력해서 정지점을 설정하거나 알고자 하는 다른 함수 명을 지정할 수 있다. 목적을 위해서 wib() function 호출 전까지 잠시 쉰다. 'list main'을 입력하면 main() 함수에 관한 소스 listing이 인쇄된다. Return 키를 다시 누르면 wib() function 호출이 21 행 상에 있음이 나타난다. 거기에 정지점을 설정 하려면 'break 21'을 입력한다. Gdb의 다음과 같이 반응한다.:


(gdb) break 21
Breakpoint 1 at 0x8048428: file eg1.c, line 21.

상단의 명령에서 요청된 행에 정지점 1이 설정 되었음을 볼 수 있다. 'run' command는 gdb가 정지할 때까지 처음부터 다시 프로그램을 실행한다. 이런 일이 발생하면 gdb는 gdb가 정지 할 정지점과 프로그램의 위치를 나타내는 다음과 같은 메시지를 생성 한다 :


Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:21
21          result = wib(value, div);

'print value'와 'print div'을 실행하면 최초의 wib() call 변수는 10과 6임을 나타내고, 'print i'는 zero를 나타낸다. 다행히 gdb는 로컬 변수(local variables) 값을 나타내며 'info locals' 명령을 사용하면 입력 내용을 줄일 수 있다.

지금까지 "value"와 "div"이 같을 때 문제가 발생 한다는 것을 알았다. 따라서 'continue'를 입력해서 다음 정지점 1에 도달할 때까지 실행을 반복한다. 'info locals'의 반복 실행은 value=9 이고 dir=7 를 나타낸다.

연속하기 전에 "value"와 "div"가 어떻게 변하는지 알기 위해 명령어 'next'로 프로그램의 싱글 스텝이 가능하다. Gdb의 응답(response)은 다음과 같다.:


(gdb) next    
22          total += result;

Return 키를 2회 이상 누르면 더하기와 빼기를 나타낸다.:


(gdb) 
23          div++;
(gdb) 
24          value--;

Return 키를 2회 누르면 21 행에 도달하여 wib()을 호출할 수 있다. 'info locals'은 "div"이 "value"와 같고 spelling forthcoming trouble이 나타난다. 나눗셈 오류를 알기 위해 'step' 명령(함수 호출을 건너 뛰는 'next'에 반대)으로 wib() 함수를 실행할 수 있다. 다음 'next' 명령을 수행하면 "result" 계산에 도달한다.

이제 디버깅이 완료 되었으며 'quit' command로 gdb는 종료된다. 실행 중인 프로그램은 이 작동으로 gdb 승인을 나타낸다.

정지점과 watchpoint의 활용

이전 예제에서 21 행에 정지점을 설정했다. wib() 함수의 호출 전에 "value"와 "div"이 같아지는 때를 알아보고자 한다. 이 지점에 도달 하려면 프로그램을 2회 연속 실행해야 한다. 그러나 정지점에 조건을 설정하면 "value"가 "div"와 같아질 때만 gdb를 중단할 수 있다. 정지점을 규정할 때 조건을 설정하기 위해 "break <line number> if <conditional expression>"을 지정할 수 있다. eg1 을 gdb에 로드 하고 다음과 같이 입력하자. :


(gdb) break 21 if value==div
Breakpoint 1 at 0x8048428: file eg1.c, line 21.

숫자 1과 같은 정지점이 이미 21 행에 규정되어 있으면 다음과 같이 'condition' command로 정지점에 조건을 설정할 수 있다.:


(gdb) condition 1 value==div

'run'으로 eg1.c를 실행하면 "value"와 "dir"이 같아질 때 gdb가 정지한다. 이렇게 되면, 같아질 때까지 수작업으로 'continue'할 필요가 없다. 정지점 조건(condition)은 C 프로그램을 디버깅 할 때 유효한 C expression이다. 실제 프로그램에 사용중인 어떤 언어에도 유효한 expression이다. 조건에 지정된 변수는 정지점이 설정된 행의 범위 안에 있어야 한다. 그렇지 않으면 expression은 의미가 없다.

Expression을 사용하지 않고 정지점 숫자를 지정하는 'condition' command로 정지점을 조건 없이(unconditional) 설정할 수 있다. 예를 들어 'condition 1'은 조건 없이 정지점 1을 설정한다.

현재 정의된 정지점과 조건이 무엇인지 알기 위해 'info break' command를 발행한다.:


(gdb) info break
Num Type           Disp Enb Address    What
1   breakpoint     keep y   0x08048428 in main at eg1.c:21
        stop only if value == div
        breakpoint already hit 1 time

A정지점 정보는 정지점의 조건, 정지점이 히트(hit)되는 횟수 그리고 'Enb' 칼럼에서의 정지점 사용 가능 여부를 결정한다. 정지점은 'disable <breakpoint number>' command를 사용하면 사용 불가가 된다. 또 'enable <breakpoint number>'은 사용 가능을 의미하며 'delete<breakpoint number>' command로 전부 삭제된다. 예를 들어 'disable 1' 명령은 포인트 1에서의 정지를 막는다.

"value"가 "div"와 같아지기 원하면 watch라 불리는 다른 타입의 정지점을 설정할 수 있다. Watchpoint는 지정된 expression이 값을 변경할 때 프로그램 실행을 멈춘다. 그러나 expression에 사용된 변수가 범위 안에 있을 때 설정해야 한다. "value"와 "div"를 범위 안에 갖기 위해 main에 정지점을 설정해서 프로그램을 실행할 수 있다. 또 main() 정지점이 히트(hit)될 때 watchpoint를 설정할 수 있다. Eg 1을 사용해서 gdb를 재시작하고 다음과 같이 입력한다.:


(gdb) break main
Breakpoint 1 at 0x8048402: file eg1.c, line 15.
(gdb) run
...
Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:15
15        value = 10;

"div"의 상황을 지속적으로 알기 위해 'watch div'를 사용할 수 있다. 그러나 "div"가 "value"와 같아질 때 정지를 원하면 다음과 같이 입력한다 :


(gdb) watch div==value
Hardware watchpoint 2: div == value

계속하면 expression "dir==value"가 값을 0(false)에서 1(true)으로 변경할 때 gdb가 정지한다 :


(gdb) continue
Continuing.
Hardware watchpoint 2: div == value

Old value = 0
New value = 1
main (argc=1, argv=0xbffff954) at eg1.c:19
19        for(i = 0; i < 10; i++)

'info locals' command는 "value"가 실제 "dir"와 같음을(8 again) 검증한다.

정의된 watchpoint는 'info watch' command(이 command는 'info break'와 동등함)를 사용하면 정지점과 같이 열거된다. 또 정지점에서 사용한 것과 동일한 신텍스를 사용하면 watchpoint의 가능, 가능금지 및 삭제가 가능하다.

Core files

gdb에서 프로그램을 실행하면 버그 트래핑(bug trapping)을 쉽게 할 수 있다. 그러나 대개의 경우 프로그램은 core 파일만 남기고 디버거 밖에서 정지된다. Gdb가 core file을 로드하면 프로그램이 정지되기 전에 프로그램의 상태를 점검할 수 있다.

프로그램 eg1을 gdb 밖에서 실행하면 core dump가 발생한다 :


$ ./eg1
Floating point exception (core dumped)

Core 파일과 함께 gdb를 시작하기 위해 쉘에서 'gdb eg1 core'나 'gdb eg1-c core' command를 실행한다. Gdb는 core file, eg1의 program listing을 로드 한다. 또 프로그램이 어떻게 종료되었는지 보여주며, gdb에서 프로그램을 실행했던 때와 유사한 메시지도 보여줄 것이다 :


...
Core was generated by `./eg1'.
Program terminated with signal 8, Floating point exception.
...
#0  0x80483ea in wib (no1=8, no2=8) at eg1.c:7
7         result = no1 / diff;

이 지점에서 'info locals', 'pront', 'info args' 및 'list'를 시행하여 제로로 나눈 값을 알 수 있다. 'info variables' command는 모든 프로그램의 변수 값을 프린트한다. 그러나 gdb가 program code에서는 물론 C library에서도 변수를 인쇄하므로 시간이 오래 걸린다. Wib()을 호출한 함수에서 무슨 일이 발생했는지 알기 위해 gdb의 stack commands를 사용할 수 있다.

스택 트레이스(Stack traces)

"call stack" 프로그램은 현재까지의 함수에 대한 리스트이다. 각 함수와 변수는 프레임 0("bottom" frame)에서 최근에 호출된 함수와 함께 "frame"으로 할당된다. 스택을 프린트하기 위해 'bt'(backtrace) command를 실행한다 :


(gdb) bt
#0  0x80483ea in wib (no1=8, no2=8) at eg1.c:7
#1  0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21

이것은 wib() 함수가 21 행의 main()에서 ('list21'로 쉽게 확인가능) 호출되었음을 나타낸다. 또 wib()은 frame 0에, main()은 frame 1에 있음을 나타낸다. 이것은wib()이 frame 0 에 있기 때문인데, frame 0는 arithmetic error가 발생할 때 프로그램 내에서 실행되는 함수이다.

'info locals' command를 시행하면 실제 gdb는 국소 변수(variables local)를 현재 프레임에 프린트한다. 이 프레임은 인터럽트 된 함수가 있는 곳에 (frame 4) 디폴트로 존재한다. 현재 프레임은 'frame' 명령으로 프린트 할 수 있다. 메인 함수(frame 1에 있는)의 변수를 알기 위해 'frame 1'후 'info locals'를 실행하면 frame 1로 스위치 할 수 있다 :


(gdb) frame 1
#1  0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21
21          result = wib(value, div);
(gdb) info locals
value = 8
div = 8
result = 4
i = 2
total = 6

이것은 "value"와 div"이 같을 때 "for" 루프 (i와 2는 같다)를 통해 오류가 세 번째 발생했음을 가리킨다.

프레임은 두 가지 방식으로 스위치 가능하다. 프레임의 숫자를 분명하게 'frame' 명령에 지정하거나 또는 스택을 위로 움직일 때는 'up', 아래로 움직일 때는 'down' command 실행한다. 프레임에 대해서 주소나 프로그램 언어 같은 자세한 정보를 원하면 'info frame' command를 실행한다.

Gdb stack commands는 core file과 프로그램 실행에 유효하다. 따라서 복잡한 프로그램의 경우에도 실행 중일 때 함수에 어떻게 도달하는 지 추적할 수 있다.

다른 프로세스에 첨부

Core file이나 프로그램의 디버깅 이외에도 gdb는 이미 실행중인 프로세스에 첨부될 수 있다. Core file name 대신, gdb를 첨부하고자 하는 프로그램의 프로세스 ID를 지정하면 된다. Loop 및 sleep를 실행한 예제 프로그램은 다음과 같다 :


#include <stdlib.h>
int main(int argc, char *argv[])
{
  int i;
  for(i = 0; i < 60; i++)
  {
    sleep(1);
  }

  return 0;
}


./eg2 &
[3] 1283

Gdb 시작해서 pid를 지정하면, 'gdb eg2 1283'이 된다. Gdb는 "1283"의 core file을 찾지 못하면 gdb는 process 1283에 첨부된다. Gdb가 어디에서 실행되더라도 관계 없다. (이 경우는 아마 sleep()에 위치한다) :


...
/home/seager/gdb/1283: No such file or directory.
Attaching to program: /home/seager/gdb/eg2, Pid 1283
...
0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6
(gdb) 

이 지점에서 유용한 gdb가 모두 발생할 수 있다. 'backtrace'를 사용해서 main()과 관련된 위치와 main()의 프레임 숫자가 무엇인지 알 수 있다. 그리고 거기에서 프레임을 스위치 하여 "for" 루프를 몇 번 통과 했는지 알 수 있다 :


(gdb) backtrace
#0  0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6
#1  0x400a877d in __sleep (seconds=1) at ../sysdeps/unix/sysv/linux/sleep.c:78
#2  0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7
(gdb) frame 2
#2  0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7
7           sleep(1);
(gdb) print i
$1 = 50

프로그램 설치가 완료되면 'detach' command로 프로그램을 실행할 수도 있고 'kill' command로 없앨(kill) 수도 있다. 또 pid 1283 아래 eg2를 첨부할 수도 있다. 이는 먼저 'file eg2'를 이용해서 그 안에 파일을 로드한 후 'attach 1283' 첨부 명령을 내림으로서 가능하다.

기타 사항

gdb를 이용하면 'detach' command로 디버깅 환경을 종료하지 않고 쉘 명령을 실행할 수 있다. 디버깅을 하면서 소스코드를 변경할 때 유용하게 쓰이는 'shell [commandline]'로서 호출되기 때문이다.

마지막으로 프로그램 실행 중에도 'set' command로 변수 값을 수정할 수 있다. Gdb에서 eg1을 실행한다. 그리고 'break 7 if diff==0' 명령으로 일부만 언급하였다. 자세한 사항은 GNU Debugger 매뉴얼을 참고하기 바란다.


참고자료

필자소개

David Seager는 IBM 소프트웨어 개발자로 2년 이상 Linux와 웹 기반의 응용 프로그램을 연구하고 있다.

출처 : http://www.ibm.com/developerworks/kr/library/l-gdb/

'프로그램언어 > C++' 카테고리의 다른 글

ListBox Control 정보 넣기.  (0) 2010.07.13
CString ->int -> CString  (1) 2010.07.09
System Error Codes (0-499)  (0) 2010.06.14
tbb::atomic testRunner  (0) 2010.05.21
RFC 1321 - MD5 Message-Digest Algorithm  (0) 2010.05.17