
코딩테스트 문제를 풀때 디버거를 잘 몰라서 printf와 같은 출력문을 반복문 사이사이에 넣어서 풀곤 했었다.
하지만 UNIX를 배우면서 Make Utility와 GCC 컴파일러에 대해 배우면서 GDB에 대해 배울 수 있었다.
이를 배우면서 흥미를 느껴 이에 대해 기록하고 앞으론 IDE에서도 디버거를 활용하려고한다.
printf를 코드 곳곳에 삽입하여 변수 값을 확인하는 방식은 간단하지만, 재귀와 같은 알고리즘이 쓰이면서 점점 복잡해지는 프로그램에서는 한계에 부딪힌다.
코드는 지저분해지고, 버그를 잡은 뒤에는 모든 printf를 다시 지워야 하는 번거로움도 존재한다.
GDB(GNU Debugger)는 리눅스(유닉스) 환경에서 이런 원시적인 방법을 대체하는 강력한 디버깅 도구이다.
프로그램을 특정 지점에서 멈추게 하고, 변수 값을 실시간으로 들여다보며, 코드를 한 줄씩 실행하는 등 프로그램의 내부를 정밀하게 분석할 수 있게 해준다.
GDB가 소스 코드의 내용을 제대로 인식하고 분석하려면, 컴파일할 때 디버깅 정보를 포함시켜야 한다. GCC 컴파일러에서 -g 옵션을 사용하면 된다.
# -g 옵션을 추가하여 컴파일한다.
gcc -g -o factorial factorial.c
-g 옵션 없이 컴파일된 실행 파일은 GDB로 분석할 수는 있지만, 소스 코드의 몇 번째 줄인지 추적하거나 변수명을 확인하는 등의 핵심 기능을 제대로 사용할 수 없다.
GDB를 사용하려면 -g 옵션을 꼭 사용해야한다.
아래는 5의 팩토리얼(!5)을 계산하는 코드이지만 버그를 포함하고 있다.
5!는 120이어야 하지만, 이 프로그램은 잘못된 결과를 출력한다.
#include <stdio.h>
// n! (n 팩토리얼)을 계산하는 함수
int calculate_factorial(int n) {
int result = 1;
// 버그: i <= n 이 되어야 하지만 i < n 으로 되어있다.
for (int i = 1; i < n; i++) {
result = result * i;
}
return result;
}
int main(void) {
int number = 5;
int result = 0;
result = calculate_factorial(number);
printf("%d의 팩토리얼 결과: %d\n", number, result);
return 0;
}
이 코드를 -g 옵션으로 컴파일하고 실행하면 5의 팩토리얼 결과: 24라는 오답이 나온다.
1) GDB 시작
터미널에 gdb 명령어와 함께 분석할 실행 파일명을 입력한다.
gdb ./factorial
GDB가 시작되면 (gdb) 프롬프트가 나타나며 명령어 입력을 기다린다.
2) 브레이크포인트(Breakpoint) 설정
브레이크포인트는 프로그램 실행 중 의도적으로 멈추고 싶은 지점을 설정하는 것이다.
가장 먼저 main 함수가 시작되는 지점에 설정해보자.
(gdb) break main
Breakpoint 1 at 0x1169: file factorial.c, line 15.
break [함수명] 또는 b [줄 번호] 형식으로 설정할 수 있다.
3) 프로그램 실행
run (축약형 r) 명령어로 프로그램을 실행한다.
프로그램은 실행되다가 이전에 설정한 브레이크포인트(main 함수)를 만나면 즉시 멈춘다.
(gdb) run
Starting program: /path/to/factorial
Breakpoint 1, main () at factorial.c:15
15 int number = 5;
이제 프로그램은 15번째 줄이 실행되기 직전에 멈춰있다.
4) 코드 한 줄씩 실행 : next
next (축약형 n) 명령어는 현재 줄을 실행하고 바로 다음 줄에서 멈춘다.
main 함수의 코드를 한 줄씩 따라가 보겠다.
(gdb) next
16 int result = 0;
(gdb) next
18 result = calculate_factorial(number);
5) 변수 값 확인 : print
print (축약형 p) 명령어는 현재 시점의 변수 값을 보여준다.
(gdb) print number
$1 = 5
(gdb) print result
$2 = 0
number 변수에는 5가, result 변수에는 0이 올바르게 들어가 있음을 확인했다.
6) 함수 내부로 들어가기: step
다음 실행할 18번 라인은 calculate_factorial 함수 호출이다.
이 함수의 내부 동작을 보고 싶다면 next 대신 step (축약형 s)을 사용한다.
step은 함수 호출을 만나면 그 함수 내부로 진입한다.
(gdb) step
calculate_factorial (n=5) at factorial.c:5
5 int result = 1;
calculate_factorial 함수 내부로 들어왔고, 파라미터 n에 5가 잘 전달된 것을 볼 수 있다.
7) 버그 추적
이제 for문 안에서 변수들이 어떻게 변하는지 next와 print로 추적해보자.
(gdb) next
8 for (int i = 1; i < n; i++) {
(gdb) next
9 result = result * i;
(gdb) print i
$3 = 1
(gdb) print result
$4 = 1
````next`를 반복하며 `i`와 `result`의 변화를 계속 관찰한다.
```gdb
# ... next를 계속 입력 ...
(gdb) next
9 result = result * i;
(gdb) p i
$7 = 4
(gdb) p result
$8 = 6
(gdb) next
8 for (int i = 1; i < n; i++) {
(gdb) p result
$9 = 24
(gdb) next
11 return result;
i가 4일 때 result는 6 * 4 = 24가 되었다.
그리고 i가 5가 되자 i < n (즉, 5 < 5) 조건이 거짓이 되어 루프를 빠져나왔다.
여기서 팩토리얼은 5까지 곱해야 하는데, i가 4일 때까지만 곱하고 루프가 종료되었음을 확인할 수 있다.
for문의 조건이 i < n이 아니라 i <= n이 되어야 한다.
8) 디버깅 종료
원인을 찾았으니 디버깅을 종료한다. quit (축약형 q) 명령어를 입력한다.
(gdb) quit
A debugging session is active.
Inferior 1 [process 12345] will be killed.
Quit anyway? (y or n) y
| 명령어 | 축약형 | 설명 |
|---|---|---|
break | b | 브레이크포인트를 설정한다. (예: b main, b 12) |
run | r | 프로그램을 실행한다. |
next | n | 다음 줄로 이동한다. (함수 안으로 들어가지 않음) |
step | s | 다음 줄로 이동한다. (함수 안으로 들어감) |
print | p | 변수의 값을 출력한다. (예: p my_variable) |
list | l | 현재 위치 주변의 소스 코드를 보여준다. |
continue | c | 다음 브레이크포인트까지 실행을 계속한다. |
quit | q | GDB를 종료한다. |