C++ GDB | "디버거" 가이드

C++ GDB | "디버거" 가이드

이 글의 핵심

C++ GDB에 대한 실전 가이드입니다.

들어가며

GDB(GNU Debugger)는 C/C++ 프로그램 디버깅의 필수 도구입니다. 중단점 설정, 변수 검사, 스택 추적 등 강력한 기능을 제공하여 버그를 빠르게 찾고 수정할 수 있습니다.


1. GDB 기본

설치

# Ubuntu/Debian
sudo apt install gdb

# macOS (LLDB 권장)
brew install gdb

# Windows (MinGW)
# MinGW 설치 시 gdb 포함

컴파일 및 실행

# 디버그 정보 포함 (-g 플래그)
g++ -g program.cpp -o program

# 최적화 끄기 (디버깅 용이)
g++ -g -O0 program.cpp -o program

# 또는 디버깅용 최적화
g++ -g -Og program.cpp -o program

# GDB 실행
gdb ./program

# 프로그램 실행
(gdb) run

# 인자와 함께 실행
(gdb) run arg1 arg2

핵심 개념:

  • -g: 디버그 정보(심볼, 라인 번호) 포함
  • -O0: 최적화 끄기 (변수가 최적화로 제거되지 않음)
  • -Og: 디버깅에 적합한 최적화 레벨

2. 기본 명령어

실행 제어

# 프로그램 실행
(gdb) run                  # 처음부터 실행
(gdb) run arg1 arg2        # 인자와 함께 실행
(gdb) continue (c)         # 다음 중단점까지 계속
(gdb) next (n)             # 다음 줄 (함수 넘김)
(gdb) step (s)             # 다음 줄 (함수 진입)
(gdb) finish               # 현재 함수 끝까지
(gdb) until                # 현재 루프 끝까지
(gdb) quit (q)             # GDB 종료

중단점 (Breakpoint)

# 중단점 설정
(gdb) break main                # 함수에 설정
(gdb) break file.cpp:42         # 파일:라인에 설정
(gdb) break MyClass::method     # 메서드에 설정
(gdb) break +5                  # 현재 위치에서 5줄 후

# 조건부 중단점
(gdb) break factorial if n == 3
(gdb) condition 1 x > 100       # 중단점 1에 조건 추가

# 중단점 관리
(gdb) info breakpoints          # 목록 확인
(gdb) delete 1                  # 중단점 1 삭제
(gdb) delete                    # 모든 중단점 삭제
(gdb) disable 1                 # 중단점 1 비활성화
(gdb) enable 1                  # 중단점 1 활성화

변수 검사

# 변수 출력
(gdb) print var                 # 변수 값 출력
(gdb) print &var                # 주소 출력
(gdb) print *ptr                # 포인터가 가리키는 값
(gdb) print arr[0]              # 배열 원소
(gdb) print obj.member          # 멤버 변수

# 자동 출력 (매 중단마다)
(gdb) display var               # 자동 출력 설정
(gdb) undisplay 1               # 자동 출력 해제

# 정보 확인
(gdb) info locals               # 지역 변수 목록
(gdb) info args                 # 함수 인자 목록
(gdb) info variables            # 전역 변수 목록

스택 추적

# 스택 추적
(gdb) backtrace (bt)            # 전체 스택
(gdb) backtrace 5               # 최근 5개 프레임
(gdb) frame 0                   # 프레임 0으로 이동
(gdb) up                        # 상위 프레임
(gdb) down                      # 하위 프레임
(gdb) info frame                # 현재 프레임 정보

3. 실전 예제

예제 1: 기본 디버깅

// program.cpp
#include <iostream>

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

int main() {
    int result = factorial(5);
    std::cout << "결과: " << result << std::endl;
    return 0;
}

GDB 세션:

# 컴파일
$ g++ -g program.cpp -o program

# GDB 실행
$ gdb ./program

# 중단점 설정
(gdb) break factorial
Breakpoint 1 at 0x1189: file program.cpp, line 5.

# 프로그램 실행
(gdb) run
Starting program: ./program
Breakpoint 1, factorial (n=5) at program.cpp:5

# 변수 확인
(gdb) print n
$1 = 5

# 다음 중단점까지 계속
(gdb) continue
Breakpoint 1, factorial (n=4) at program.cpp:5

# 스택 추적
(gdb) backtrace
#0  factorial (n=4) at program.cpp:5
#1  0x0000555555555195 in factorial (n=5) at program.cpp:6
#2  0x00005555555551b5 in main () at program.cpp:10

# 종료
(gdb) quit

예제 2: 조건부 중단점

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    for (int i = 0; i < numbers.size(); ++i) {
        int value = numbers[i] * 2;
        std::cout << value << std::endl;
    }
    
    return 0;
}

GDB 세션:

# 조건부 중단점: i가 5일 때만
(gdb) break 7 if i == 5
(gdb) run
# i가 5일 때만 중단됨

# 조건 확인
(gdb) print i
$1 = 5
(gdb) print value
$2 = 12

예제 3: 워치포인트 (변수 감시)

#include <iostream>

int main() {
    int counter = 0;
    
    for (int i = 0; i < 10; ++i) {
        counter += i;
        if (counter > 20) {
            counter = 0;  // 버그: 여기서 리셋됨
        }
    }
    
    std::cout << "최종: " << counter << std::endl;
    return 0;
}

GDB 세션:

# 워치포인트 설정 (counter 값 변경 시 중단)
(gdb) watch counter
(gdb) run

# counter가 변경될 때마다 중단
Hardware watchpoint 2: counter
Old value = 0
New value = 1

# 계속 실행하면서 변경 추적
(gdb) continue

예제 4: 코어 덤프 분석

#include <iostream>

void crash() {
    int* ptr = nullptr;
    *ptr = 42;  // Segmentation fault!
}

int main() {
    crash();
    return 0;
}

코어 덤프 분석:

# 코어 덤프 활성화
$ ulimit -c unlimited

# 프로그램 실행
$ ./program
Segmentation fault (core dumped)

# GDB로 코어 덤프 분석
$ gdb ./program core

# 크래시 위치 확인
(gdb) backtrace
#0  0x0000555555555189 in crash () at program.cpp:5
#1  0x00005555555551a5 in main () at program.cpp:10

# 크래시 프레임으로 이동
(gdb) frame 0
#0  0x0000555555555189 in crash () at program.cpp:5
5           *ptr = 42;

# 변수 확인
(gdb) print ptr
$1 = (int *) 0x0

4. 고급 기능

메모리 검사

# 메모리 내용 확인 (x = examine)
(gdb) x/10x address     # 16진수로 10개 워드
(gdb) x/10d address     # 10진수로 10개 워드
(gdb) x/10c address     # 문자로 10개
(gdb) x/s address       # 문자열로 출력
(gdb) x/10i address     # 명령어 10개 (어셈블리)

# 예제
(gdb) print &var
$1 = (int *) 0x7fffffffe3fc
(gdb) x/4x 0x7fffffffe3fc
0x7fffffffe3fc: 0x0000000a 0x00000000 0xf7dc2620 0x00007fff

타입 정보

# 타입 확인
(gdb) ptype var         # 상세한 타입 정보
(gdb) whatis var        # 간단한 타입

# 예제
(gdb) ptype std::vector<int>
type = class std::vector<int, std::allocator<int>> {
  ...
}

멀티스레드 디버깅

# 스레드 목록
(gdb) info threads
  Id   Target Id         Frame
* 1    Thread 0x7ffff7fc0740 (LWP 12345) main () at main.cpp:10
  2    Thread 0x7ffff6fbf700 (LWP 12346) worker () at worker.cpp:5

# 스레드 전환
(gdb) thread 2
[Switching to thread 2 (Thread 0x7ffff6fbf700)]

# 현재 스레드 스택
(gdb) backtrace

# 모든 스레드 스택
(gdb) thread apply all backtrace

역방향 디버깅 (Reverse Debugging)

# 기록 시작
(gdb) record
(gdb) continue

# 역방향 실행
(gdb) reverse-step      # 이전 줄로
(gdb) reverse-next      # 이전 줄로 (함수 넘김)
(gdb) reverse-continue  # 이전 중단점까지
(gdb) reverse-finish    # 함수 시작까지

5. 자주 발생하는 문제

문제 1: 디버그 정보 없음

# ❌ 디버그 정보 없음
$ g++ program.cpp -o program
$ gdb ./program
(gdb) list
No symbol table is loaded.

# ✅ -g 플래그 추가
$ g++ -g program.cpp -o program
$ gdb ./program
(gdb) list
1       #include <iostream>
2
3       int factorial(int n) {
...

해결책: 항상 -g 플래그로 컴파일하세요.

문제 2: 최적화로 인한 변수 제거

#include <iostream>

int main() {
    int x = 10;
    int y = x * 2;
    int z = y + 5;
    std::cout << z << std::endl;
    return 0;
}
# ❌ -O3 최적화
$ g++ -g -O3 program.cpp -o program
$ gdb ./program
(gdb) break main
(gdb) run
(gdb) print x
$1 = <optimized out>  # 변수가 최적화로 제거됨

# ✅ -O0 또는 -Og
$ g++ -g -O0 program.cpp -o program
$ gdb ./program
(gdb) print x
$1 = 10  # 정상 출력

해결책: 디버깅 시 -O0 또는 -Og 사용하세요.

문제 3: 심볼 제거 (strip)

# ❌ strip 실행 후
$ strip program
$ gdb ./program
(gdb) break main
Function "main" not defined.

# ✅ strip 하지 않기
# 또는 별도 심볼 파일 유지
$ objcopy --only-keep-debug program program.debug
$ strip program
$ objcopy --add-gnu-debuglink=program.debug program

해결책: 디버그 빌드는 strip하지 마세요.

문제 4: 멀티스레드 디버깅

#include <iostream>
#include <thread>

void worker(int id) {
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread " << id << ": " << i << std::endl;
    }
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    
    t1.join();
    t2.join();
    
    return 0;
}

GDB 세션:

# 중단점 설정
(gdb) break worker

# 실행
(gdb) run
[New Thread 0x7ffff6fbf700 (LWP 12346)]
Thread 2 "program" hit Breakpoint 1, worker (id=1) at program.cpp:5

# 스레드 목록
(gdb) info threads
  Id   Target Id         Frame
* 2    Thread 0x7ffff6fbf700 (LWP 12346) worker (id=1) at program.cpp:5
  1    Thread 0x7ffff7fc0740 (LWP 12345) 0x00007ffff7bc0a9d in __pthread_join

# 다른 스레드로 전환
(gdb) thread 1
(gdb) backtrace

6. TUI 모드

TUI(Text User Interface)는 소스 코드를 화면에 표시하는 모드입니다.

# TUI 모드로 시작
$ gdb -tui ./program

# 또는 실행 중 활성화
(gdb) tui enable
(gdb) tui disable

# 레이아웃 변경
(gdb) layout src        # 소스 코드
(gdb) layout asm        # 어셈블리
(gdb) layout split      # 소스 + 어셈블리
(gdb) layout regs       # 레지스터 + 소스

# 창 전환
Ctrl+X, A               # TUI 모드 토글
Ctrl+X, O               # 활성 창 전환
Ctrl+L                  # 화면 새로고침

7. 실전 예제: 버그 찾기

#include <iostream>
#include <vector>

double average(const std::vector<int>& numbers) {
    int sum = 0;
    for (int num : numbers) {
        sum += num;
    }
    return sum / numbers.size();  // 버그: 정수 나눗셈!
}

int main() {
    std::vector<int> scores = {85, 92, 78, 95, 88};
    double avg = average(scores);
    std::cout << "평균: " << avg << std::endl;
    return 0;
}

GDB로 버그 찾기:

# 컴파일 및 실행
$ g++ -g bug.cpp -o bug
$ ./bug
평균: 87  # 예상: 87.6

# GDB 실행
$ gdb ./bug

# average 함수에 중단점
(gdb) break average
(gdb) run
Breakpoint 1, average (numbers=...) at bug.cpp:5

# 변수 확인
(gdb) next
(gdb) next
...
(gdb) print sum
$1 = 438

(gdb) print numbers.size()
$2 = 5

# 나눗셈 전 타입 확인
(gdb) ptype sum
type = int
(gdb) ptype numbers.size()
type = std::size_t

# 문제 발견: int / size_t = int (정수 나눗셈)
# 해결: static_cast<double>(sum) / numbers.size()

8. GDB 명령어 요약

카테고리명령어설명
실행run프로그램 실행
continue (c)다음 중단점까지
next (n)다음 줄 (함수 넘김)
step (s)다음 줄 (함수 진입)
finish현재 함수 끝까지
중단점break중단점 설정
watch워치포인트 설정
info breakpoints중단점 목록
delete중단점 삭제
검사print변수 출력
display자동 출력
info locals지역 변수
backtrace (bt)스택 추적
메모리x메모리 검사
ptype타입 정보
스레드info threads스레드 목록
thread스레드 전환

정리

핵심 요약

  1. GDB: C/C++ 디버깅 도구
  2. -g 플래그: 디버그 정보 포함 필수
  3. 중단점: break 명령으로 설정
  4. 변수 검사: print, display로 확인
  5. 스택 추적: backtrace로 호출 스택 확인
  6. 조건부 중단: break ... if 조건 지정
  7. 워치포인트: watch로 변수 변경 감시
  8. 코어 덤프: 크래시 원인 분석

실전 팁

컴파일:

  • 디버깅 시 -g -O0 또는 -g -Og 사용
  • 릴리스 빌드는 -O2 또는 -O3
  • strip은 릴리스 빌드에만 사용

디버깅:

  • 조건부 중단점으로 특정 상황만 검사
  • watch로 예상치 못한 변수 변경 추적
  • TUI 모드로 소스 코드 확인하며 디버깅
  • backtrace로 크래시 위치 빠르게 파악

효율:

  • 중단점을 최소화하여 빠르게 문제 위치 좁히기
  • display로 관심 변수 자동 출력
  • 멀티스레드는 info threads로 전체 상태 파악

다음 단계

  • C++ Valgrind
  • C++ Sanitizers
  • C++ 예외 성능

관련 글

  • C++ 디버깅 완벽 가이드 | GDB·Sanitizer·메모리 누수·멀티스레드 디버깅 실전
  • C++ 디버깅 실전 가이드 | gdb, LLDB, Visual Studio 완벽 활용
  • C++ Sanitizers |
  • C++ Valgrind |
  • C++ Benchmarking |