GDB로 C++ 디버깅하기: 브레이크포인트, 워치포인트, backtrace, Core Dump 분석

printf 디버깅의 한계

cout 100개를 찍어도 버그를 못 찾을 때

세그폴트가 발생하는 버그를 찾고 있었습니다. std::cout을 수십 개 추가했지만 출력이 버퍼링되어 정확한 크래시 위치를 알 수 없었습니다. GDB(GNU Debugger)는 “어느 줄에서 멈췄는지, 그때 변수 값과 호출 스택이 어떤지”를 멈춘 상태에서 직접 볼 수 있게 해 줍니다. 브레이크포인트(실행을 멈출 지점)·워치포인트(변수 변경 감지)·백트레이스(호출 스택)·print(변수 출력)·step/next/continue(단계별 실행)만 익혀도 printf 디버깅보다 훨씬 빠르게 버그 위치를 좁힐 수 있습니다. 요구 환경: GDB(Linux/WSL: apt install gdb 또는 sudo yum install gdb). 디버그 정보가 있어야 하므로 빌드 시 -g 옵션 사용(g++ -g, CMake에서는 Debug 구성).


GDB가 필요한 다섯 가지 버그 상황

배열 범위 초과로 인한 세그폴트

상황: 10만 개 원소를 처리하는 루프에서 가끔 크래시가 납니다. cout으로 i를 찍어봤지만 출력이 버퍼링되어 정확한 i 값을 알 수 없습니다.

// buggy_array.cpp
#include <iostream>
void processData(int* arr, int size) {
    for (int i = 0; i <= size; ++i) {  // ❌ <= 버그 (i==size일 때 범위 초과)
        arr[i] = i * 2;
    }
}
int main() {
    int arr[100];
    processData(arr, 100);  // 크래시!
    std::cout << "done\n";
    return 0;
}

GDB로 해결: run → 크래시 → backtrace → print i → i=100 발견 → i <= size가 i < size여야 함을 확인.

널 포인터 역참조

상황: 외부 라이브러리에서 받은 포인터가 가끔 null인데, 어디서 null이 들어오는지 모릅니다.

// null_ptr.cpp
struct Node { int value; Node* next; };
int sumList(Node* head) {
    int sum = 0;
    while (head != nullptr) {
        sum += head->value;  // head가 null이면 크래시
        head = head->next;
    }
    return sum;
}

GDB로 해결: break sumList → run → print head → null이면 호출자 확인.

끝나지 않는 무한 루프

상황: 프로그램이 멈춘 것처럼 보입니다. i++를 누락했을 가능성이 있습니다.

// infinite_loop.cpp
void processData() {
    int i = 0;
    while (i < 100) {
        process(i);
        // i++ 누락!
    }
}

GDB로 해결: Ctrl+C로 중단 → backtrace → print i → i가 변하지 않음 확인.

어디선가 값이 덮어씌워지는 메모리 오염

상황: arr[5]가 0이어야 하는데 어딘가에서 999로 바뀝니다. 어디서 바뀌는지 모릅니다.

// memory_corruption.cpp
int arr[10] = {0};
someFunction();  // 이 함수 내부 어딘가에서 arr[5] 오염

GDB로 해결: watch arr[5] → continue → 값이 변경되는 시점에서 멈춤.

1000번 중 500번째에만 나는 버그

상황: 루프가 1000번 돌 때 500번째에서만 크래시합니다. 매번 next로 500번 진행하는 것은 비효율적입니다.

// conditional_bug.cpp
for (int i = 0; i < 1000; ++i) {
    process(i);  // i=500일 때만 버그
}

GDB로 해결: break main.cpp:10 if i == 500 → run → 500번째에서만 멈춤.

상황별 GDB 기능 요약 다이어그램

flowchart TB
    subgraph problems[문제 유형]
        P1[세그폴트]
        P2[널 포인터]
        P3[무한 루프]
        P4[메모리 오염]
        P5[조건부 버그]
    end
    subgraph gdb_tools[GDB 도구]
        G1[backtrace]
        G2[print]
        G3[watch]
        G4[조건부 break]
    end
    P1 --> G1
    P1 --> G2
    P2 --> G2
    P3 --> G1
    P3 --> G2
    P4 --> G3
    P5 --> G4

-g로 디버그 빌드하고 GDB 시작하기

디버그 빌드

-g 옵션을 주면 실행 파일에 디버그 심볼(소스 줄 번호, 변수 이름)이 들어가서, GDB에서 “어느 줄에서 멈췄는지”, “변수 이름으로 값을 보는 것”이 가능해집니다. -O0로 최적화를 끄면 변수가 최적화로 사라지거나 코드 순서가 바뀌는 일이 줄어듭니다.

# 디버그 정보 포함 (-g), 최적화 끄기 (-O0)
g++ -g -O0 main.cpp -o myapp
# CMake 사용 시
cmake -DCMAKE_BUILD_TYPE=Debug ..
make

GDB 시작

# 프로그램 로드
gdb ./myapp
# GDB 프롬프트에서 실행
(gdb) run
# 인자와 함께 실행
(gdb) run arg1 arg2
# 종료
(gdb) quit

GDB 워크플로우

sequenceDiagram
    participant Dev as 개발자
    participant GDB as GDB
    participant App as 대상 프로그램
    Dev->>GDB: gdb ./myapp
    GDB->>App: 로드 (디버그 심볼)
    Dev->>GDB: break main
    Dev->>GDB: run
    GDB->>App: 실행 시작
    App->>GDB: main() 도달 → 중단
    GDB->>Dev: 프롬프트 반환
    Dev->>GDB: next / step / print
    GDB->>Dev: 결과 출력
    Dev->>GDB: continue
    GDB->>App: 다음 브레이크포인트까지 실행

브레이크포인트: 원하는 줄에서 멈추기

브레이크포인트란?

브레이크포인트는 프로그램 실행이 특정 위치에 도달했을 때 자동으로 멈추게 하는 지점입니다. printf를 여러 개 넣는 대신, 한 번 설정하면 해당 줄에 도달할 때마다 멈춥니다.

기본 브레이크포인트 설정

# 함수에 브레이크포인트
(gdb) break main
Breakpoint 1 at 0x401234: file main.cpp, line 5.
(gdb) break processData
Breakpoint 2 at 0x401256: file main.cpp, line 12.
# 파일:라인에 브레이크포인트
(gdb) break main.cpp:15
Breakpoint 3 at 0x401278: file main.cpp, line 15.

조건부 브레이크포인트

# i가 50일 때만 멈춤
(gdb) break main.cpp:20 if i == 50
# ptr이 null일 때만 멈춤
(gdb) break processData if ptr == nullptr
# size가 0보다 클 때만 멈춤
(gdb) break main.cpp:10 if size > 0

브레이크포인트 관리

# 브레이크포인트 목록
(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000401234 in main at main.cpp:5
2       breakpoint     keep y   0x0000000000401256 in processData at main.cpp:12
# 브레이크포인트 삭제
(gdb) delete 1          # 1번 삭제
(gdb) delete            # 모두 삭제
# 브레이크포인트 비활성화/활성화
(gdb) disable 2
(gdb) enable 2
# 위치로 삭제
(gdb) clear main.cpp:15

브레이크포인트 동작 원리

GDB는 일반 사용자 프로그램이라 다른 프로세스를 직접 멈출 권한이 없습니다. Linux에서는 ptrace(2) 시스템 콜로 커널에 “이 프로세스의 추적자가 되겠다”고 등록하고, 이후 대상 프로세스의 메모리·레지스터 읽기/쓰기와 실행 재개를 전부 커널을 통해 합니다.

소프트웨어 브레이크포인트는 이 권한으로 대상 주소의 첫 바이트를 INT3(x86-64에서 0xCC) 명령어로 덮어쓰는 방식입니다. CPU가 그 주소에 도달하면 트랩이 발생하고, 커널이 프로세스를 멈춘 뒤 GDB에 알립니다. GDB는 보관해 둔 원래 바이트를 되돌리고, 한 명령어만 실행한 다음 다시 0xCC를 써 넣고 계속 실행합니다. 이 구조를 알면 몇 가지 현상이 이해됩니다.

  • 브레이크포인트를 수백 개 걸어도 멈추지 않는 동안에는 거의 느려지지 않습니다. 코드가 바뀔 뿐 감시하는 게 아니기 때문입니다. 반대로 조건부 브레이크포인트는 도달할 때마다 멈췄다가 GDB가 조건을 평가하고 재개하므로, 뜨거운 루프에 걸면 눈에 띄게 느려집니다.
  • 다른 프로세스에 붙는(gdb -p) 경우 커널 보안 정책이 ptrace를 막을 수 있습니다(아래 “ptrace: Operation not permitted” 항목).
  • 워치포인트는 코드를 고치는 방식으로는 만들 수 없어서 CPU의 디버그 레지스터를 씁니다. 그래서 개수 제한이 있습니다(아래 “워치포인트 제한”).

실습: 브레이크포인트로 배열 버그 찾기

// breakpoint_demo.cpp - g++ -g -O0 -o bp_demo breakpoint_demo.cpp
#include <iostream>
void fillArray(int* arr, int size) {
    for (int i = 0; i <= size; ++i) {  // 버그: <=
        arr[i] = i;
    }
}
int main() {
    int arr[5];
    fillArray(arr, 5);
    std::cout << "done\n";
    return 0;
}
# 1. 빌드
$ g++ -g -O0 -o bp_demo breakpoint_demo.cpp
# 2. GDB 실행
$ gdb ./bp_demo
# 3. fillArray 함수에 브레이크포인트
(gdb) break fillArray
# 4. 실행
(gdb) run
# 5. 브레이크포인트에서 멈춤. next로 한 줄씩 진행
(gdb) next
(gdb) next
# i가 5일 때 arr[5] 접근 → 다음 next에서 세그폴트
# 6. 또는 조건부 브레이크로 i==5에서만 멈춤
(gdb) run
(gdb) break fillArray
(gdb) run
(gdb) break fillArray if i == 5
(gdb) continue

워치포인트: 값이 바뀌는 순간 멈추기

워치포인트란?

워치포인트는 특정 변수나 메모리 주소의 값이 변경될 때 실행을 멈추게 하는 기능입니다. “어디서 이 변수가 바뀌는지” 모를 때 유용합니다.

기본 워치포인트

# 변수 변경 시 멈춤
(gdb) watch variable_name
# 포인터가 가리키는 값 변경 시 멈춤
(gdb) watch *ptr
# 배열 요소 변경 시
(gdb) watch arr[5]

워치포인트 종류

# watch: 쓰기 시 멈춤
(gdb) watch x
# rwatch: 읽기 시 멈춤 (하드웨어 지원 필요)
(gdb) rwatch x
# awatch: 읽기 또는 쓰기 시 멈춤
(gdb) awatch x

실습: 메모리 오염 추적

// watchpoint_demo.cpp - g++ -g -O0 -o wp_demo watchpoint_demo.cpp
#include <iostream>
void corruptData(int* arr) {
    arr[5] = 999;  // 여기서 arr[5] 변경
}
int main() {
    int arr[10] = {0};
    std::cout << "arr[5] before: " << arr[5] << "\n";
    corruptData(arr);  // arr[5]가 여기서 바뀜
    std::cout << "arr[5] after: " << arr[5] << "\n";
    return 0;
}
# 1. main에 브레이크포인트 후 실행
$ gdb ./wp_demo
(gdb) break main
(gdb) run
# 2. arr[5]에 워치포인트 설정 (main에서 arr이 유효한 상태에서)
(gdb) watch arr[5]
Hardware watchpoint 2: arr[5]
# 3. continue로 진행
(gdb) continue
# 4. arr[5]가 변경되면 멈춤
Hardware watchpoint 2: arr[5]
Old value = 0
New value = 999
corruptData (arr=0x7fff...) at watchpoint_demo.cpp:6
6       arr[5] = 999;
# 5. backtrace로 호출 경로 확인
(gdb) backtrace
#0  corruptData (arr=0x7fff...) at watchpoint_demo.cpp:6
#1  main () at watchpoint_demo.cpp:14

워치포인트 제한

  • 하드웨어 워치포인트: CPU가 지원하면 개수 제한 있음(보통 4개). 매우 빠름.
  • 소프트웨어 워치포인트: 개수 제한 없지만 매 단계마다 확인하여 느림.
  • 지역 변수는 스코프를 벗어나면 워치포인트가 자동 해제될 수 있음.

backtrace로 크래시 지점의 호출 스택 보기

백트레이스란?

백트레이스(또는 스택 트레이스)는 “현재 실행 위치에 도달하기까지 어떤 함수들이 호출되었는지” 호출 스택을 보여줍니다. 크래시 시 어디서 문제가 발생했는지, 누가 그 함수를 호출했는지 파악하는 데 필수입니다.

기본 백트레이스

# 호출 스택 보기
(gdb) backtrace
(gdb) bt
# 출력 예시:
#0  buggyFunction (arr=0x7fff..., size=10) at main.cpp:5
#1  main () at main.cpp:12

상세 백트레이스 (모든 프레임의 지역 변수)

(gdb) backtrace full
(gdb) bt full
# 출력 예시:
#0  buggyFunction (arr=0x7fff..., size=10) at main.cpp:5
    i = 10
    size = 10
#1  main () at main.cpp:12
    arr = {0, 2, 4, ...}

프레임 이동

# 2번 프레임으로 이동
(gdb) frame 2
(gdb) f 2
# 현재 프레임 정보
(gdb) info frame
# 위/아래 프레임으로 이동
(gdb) up
(gdb) down

실습: 세그폴트에서 백트레이스 활용

// backtrace_demo.cpp - g++ -g -O0 -o bt_demo backtrace_demo.cpp
#include <iostream>
void level3(int* p) {
    *p = 42;  // p가 null이면 크래시
}
void level2(int* p) {
    level3(p);
}
void level1(int* p) {
    level2(p);
}
int main() {
    int* p = nullptr;
    level1(p);  // 크래시!
    return 0;
}
$ g++ -g -O0 -o bt_demo backtrace_demo.cpp
$ gdb ./bt_demo
(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x0000000000401156 in level3 (p=0x0) at backtrace_demo.cpp:5
5       *p = 42;
# 백트레이스로 호출 경로 확인
(gdb) backtrace
#0  level3 (p=0x0) at backtrace_demo.cpp:5
#1  level2 (p=0x0) at backtrace_demo.cpp:9
#2  level1 (p=0x0) at backtrace_demo.cpp:13
#3  main () at backtrace_demo.cpp:18
# main 프레임으로 이동해 p 확인
(gdb) frame 3
(gdb) print p
$1 = (int *) 0x0
# level3에서 p 확인
(gdb) frame 0
(gdb) print p
$2 = (int *) 0x0

스택 프레임 구조

flowchart TB
    subgraph stack["호출 스택 (아래→위)"]
        F0["main() - 프레임 3"]
        F1["level1() - 프레임 2"]
        F2["level2() - 프레임 1"]
        F3["level3() - 프레임 0 (현재)"]
    end
    F0 --> F1
    F1 --> F2
    F2 --> F3
    subgraph vars[프레임 0 지역 변수]
        V1["p = 0x0"]
    end

print·x로 변수와 메모리 검사하기

# 변수 값 출력
(gdb) print x
(gdb) p x
# 포인터 역참조
(gdb) print *ptr
# 구조체/클래스 멤버
(gdb) print person.name
(gdb) print obj->value
# 배열
(gdb) print arr[0]
(gdb) print arr[0]@10   # 10개 원소 (GDB)

다양한 print 형식

# 16진수
(gdb) print/x ptr
# 10진수
(gdb) print/d x
# 문자
(gdb) print/c c
# 부동소수점
(gdb) print/f f

info locals, info args

# 현재 프레임의 모든 지역 변수
(gdb) info locals
# 현재 함수의 인자
(gdb) info args

메모리 검사 (x 명령)

# x/[개수][형식][크기] 주소
# 형식: x(16진), d(10진), s(문자열), i(명령어)
# 크기: b(1바이트), h(2바이트), w(4바이트), g(8바이트)
# 16진수로 10개 워드
(gdb) x/10x ptr
# 10진수로 10개
(gdb) x/10d ptr
# 문자열로
(gdb) x/10s ptr

실습: print로 버그 원인 확인

// print_demo.cpp - g++ -g -O0 -o print_demo print_demo.cpp
#include <iostream>
struct Point { int x, y; };
void process(Point* p) {
    if (p == nullptr) return;
    p->x *= 2;
    p->y *= 2;
}
int main() {
    Point pt = {10, 20};
    process(&pt);
    std::cout << pt.x << ", " << pt.y << "\n";
    return 0;
}
$ gdb ./print_demo
(gdb) break process
(gdb) run
# process 내부에서 p 검사
(gdb) print p
$1 = (Point *) 0x7fffffffe2a0
(gdb) print *p
$2 = {x = 10, y = 20}
(gdb) print p->x
$3 = 10
(gdb) next
(gdb) print p->x
$4 = 20

step·next·continue로 한 줄씩 실행하기

step vs next vs continue

명령동작
next (n)다음 소스 줄로 이동. 함수 호출이 있으면 함수 안으로 들어가지 않고 한 번에 실행
step (s)다음 소스 줄로 이동. 함수 호출이 있으면 함수 안으로 들어감
continue (c)다음 브레이크포인트까지 계속 실행
finish현재 함수가 반환될 때까지 실행

단계별 실행 흐름

flowchart TD
    A[현재 위치] --> B{next vs step?}
    B -->|next| C[다음 소스 줄로]
    B -->|step| D{함수 호출?}
    D -->|예| E[함수 내부로 진입]
    D -->|아니오| C
    C --> F[다음 명령 대기]
    E --> F
    F --> G{finish?}
    G -->|예| H[현재 함수 반환까지 실행]
    G -->|아니오| A
    H --> F

실습: step vs next

// stepping_demo.cpp - g++ -g -O0 -o step_demo stepping_demo.cpp
#include <iostream>
int add(int a, int b) {
    return a + b;
}
int main() {
    int x = 10;
    int y = 20;
    int z = add(x, y);   // 이 줄에서 next vs step 차이
    std::cout << z << "\n";
    return 0;
}
$ gdb ./step_demo
(gdb) break main
(gdb) run
# next: add() 안으로 들어가지 않음
(gdb) next
(gdb) next
(gdb) next   # add(x, y) 호출 후 다음 줄(std::cout)로
(gdb) print z
$1 = 30
# step: add() 안으로 들어감
(gdb) run
(gdb) next
(gdb) next
(gdb) step   # add() 함수 내부로 진입
add (a=10, b=20) at stepping_demo.cpp:4
4       return a + b;
(gdb) print a
$2 = 10
(gdb) print b
$3 = 20
(gdb) finish  # add() 반환까지 실행
(gdb) print z
$4 = 30

N번 반복 실행

# next 5번 실행
(gdb) next 5
# step 3번 실행
(gdb) step 3

continue 사용

# 브레이크포인트 여러 개 설정 후
(gdb) break main
(gdb) break processData
(gdb) run
# main에서 멈춤
(gdb) continue
# processData에서 멈춤
(gdb) continue
# 다음 브레이크포인트 또는 프로그램 종료까지

버그 하나를 처음부터 끝까지 GDB로 찾기

전체 예제 코드

// full_demo.cpp - g++ -g -O0 -o full_demo full_demo.cpp
#include <iostream>
#include <vector>
void processVector(std::vector<int>& vec) {
    for (size_t i = 0; i <= vec.size(); ++i) {  // ❌ <= 버그
        vec[i] *= 2;
    }
}
int main() {
    std::vector<int> vec = {1, 2, 3};
    processVector(vec);
    std::cout << "done\n";
    return 0;
}

단계별 GDB 디버깅

# 1. 빌드
$ g++ -g -O0 -o full_demo full_demo.cpp
# 2. GDB 시작
$ gdb ./full_demo
# 3. processVector에 브레이크포인트
(gdb) break processVector
# 4. 실행
(gdb) run
# 5. next로 루프 진행 (또는 조건부 break)
(gdb) next
(gdb) next
# ... i가 3일 때 vec[3] 접근 → 세그폴트
# 6. 크래시 후
(gdb) backtrace
#0  processVector (vec=...) at full_demo.cpp:5
#1  main () at full_demo.cpp:12
(gdb) print i
$1 = 3
(gdb) print vec.size()
$2 = 3
# 7. 원인: i <= vec.size() → i가 3일 때 vec[3] 접근 (범위 초과)
#    수정: i < vec.size()

이 실습에서 쓴 GDB 명령

- [ ] break [위치] — 브레이크포인트 설정
- [ ] run — 실행
- [ ] next / step — 단계별 실행
- [ ] continue — 다음 브레이크포인트까지
- [ ] backtrace — 호출 스택
- [ ] print [변수] — 변수 값
- [ ] watch [변수] — 변경 감지
- [ ] frame N — 프레임 이동
- [ ] info locals / info args — 지역 변수/인자

스레드·STL 출력·TUI·역방향 디버깅

기본 명령에 익숙해진 다음 실무에서 가장 자주 필요해지는 기능들입니다.

멀티스레드 프로그램

(gdb) info threads                 # 스레드 목록, * 표시가 현재 스레드
(gdb) thread 3                     # 3번 스레드로 전환 (이후 bt, print가 이 스레드 기준)
(gdb) thread apply all bt          # 모든 스레드의 호출 스택 — 데드락 분석의 출발점
(gdb) set scheduler-locking step   # step/next 하는 동안 다른 스레드는 멈춰 둠

멀티스레드 프로그램에서 next를 누르면 다른 스레드가 같은 브레이크포인트에 걸려서 엉뚱한 스레드로 화면이 넘어가는 일이 흔합니다. 한 줄씩 따라가던 흐름이 끊기고, 방금 본 변수 값이 다른 스레드의 것이 되어 혼란스럽습니다. set scheduler-locking step을 켜면 스텝하는 동안 나머지 스레드를 멈춰 두므로 한 스레드의 흐름만 따라갈 수 있습니다. 다만 다른 스레드가 쥔 락을 기다리는 코드에서는 영원히 진행되지 않으니, 그때는 off로 되돌립니다. 데드락 의심 상황에서는 스텝보다 thread apply all bt로 모든 스레드가 어디서 무엇을 기다리는지를 한 번에 보는 편이 훨씬 빠릅니다.

타입 확인과 STL 출력

(gdb) whatis config                # 간단한 타입 이름
(gdb) ptype config                 # 멤버와 메서드까지 펼친 타입 정의
(gdb) set print pretty on          # 구조체를 들여쓰기해 출력
(gdb) print vec                    # std::vector<int> of length 3 = {1, 2, 3}

print vec의 결과가 {1, 2, 3} 대신 _M_impl, _M_start 같은 내부 멤버로 쏟아진다면 libstdc++ pretty printer가 로드되지 않은 것입니다. 배포판 GDB는 보통 자동으로 로드하지만, 직접 빌드한 GCC나 크로스 툴체인을 쓰면 빠지기 쉽습니다. info pretty-printer로 등록 여부를 확인하고, 없으면 해당 GCC의 share/gcc-*/python 경로를 .gdbinit에서 python 블록으로 등록합니다.

TUI 모드: 소스를 보면서 디버깅

$ gdb -tui ./myapp                 # 처음부터 TUI로 시작
(gdb) layout src                   # 소스 창 (asm, split, regs도 가능)
# Ctrl+X, A : TUI 켜기/끄기
# Ctrl+X, O : 소스 창 ↔ 명령 창 포커스 전환
# Ctrl+L    : 화면이 깨졌을 때 다시 그리기

list를 반복하지 않아도 현재 줄이 소스 창에 표시되어 IDE 없이 원격 서버에서 디버깅할 때 편합니다. 처음 쓰면 화살표 키가 명령 기록이 아니라 소스 창 스크롤로 동작해서 당황하게 되는데, 포커스가 소스 창에 있기 때문입니다. focus cmd(또는 Ctrl+X, O)로 명령 창에 포커스를 주면 평소처럼 이전 명령을 불러올 수 있습니다. 프로그램이 표준 출력으로 글자를 찍으면 화면이 깨지는데, 이때 Ctrl+L로 다시 그립니다.

역방향 디버깅: 크래시 직전으로 되감기

(gdb) break processData
(gdb) run
(gdb) record                       # 여기서부터 실행 기록 시작
(gdb) continue                     # 크래시 발생
(gdb) reverse-next                 # 한 줄 뒤로
(gdb) reverse-step                 # 함수 안으로 들어가며 뒤로
(gdb) reverse-continue             # 이전 브레이크포인트/워치포인트까지 되감기
(gdb) reverse-finish               # 현재 함수를 호출한 지점까지

“값이 언제 망가졌는지”를 찾을 때 강력합니다. 크래시 지점에서 망가진 변수에 watch를 걸고 reverse-continue하면 그 값을 마지막으로 쓴 줄에서 멈춥니다. 하지만 GDB 내장 record는 명령어 하나하나의 변화를 기록하므로 수십~수백 배 느려지고, 기본 기록 한도(record full insn-number-max, 200,000 명령어)가 금방 차서 기록을 계속할지 묻습니다. 또 최신 glibc의 memcpy·strlen처럼 AVX 명령어를 쓰는 코드에서 Process record does not support instruction 0xc5 ... 에러로 멈추는 경우가 자주 있어서, 기록 구간을 문제 함수 근처로 좁혀야 실용적입니다. 긴 실행을 되감아야 하거나 가끔만 재현되는 버그라면, 실행을 한 번 녹화한 뒤 GDB 명령 그대로 몇 번이고 정·역방향 재생할 수 있는 rr이 더 현실적인 선택입니다(Linux x86-64, 성능 카운터 접근이 필요해 일부 VM에서는 동작하지 않음).


No symbol table·optimized out 등 GDB 에러 해결

”No symbol table” / “No debugging symbols found”

원인: -g 옵션 없이 빌드함. 해결법:

# 재빌드 시 -g 추가
g++ -g -O0 main.cpp -o myapp
# 기존 바이너리에 심볼 있는지 확인
file myapp
# "not stripped" 또는 "with debug_info" 확인

“Cannot access memory at address 0x0”

원인: Null 포인터 역참조. 해결법:

(gdb) backtrace
(gdb) frame 0
(gdb) info args
(gdb) print ptr   # 0x0인지 확인

“optimized out” (변수 값이 표시되지 않음)

원인: -O2, -O3 등 최적화로 변수가 제거되거나 레지스터에만 있음. -g가 만드는 디버그 정보(DWARF)는 “이 변수가 이 주소 구간에서는 이 레지스터에 있다”는 식으로 위치를 기록하는데, 최적화로 변수가 아예 사라지거나 값이 다른 계산에 합쳐지면 기록할 위치가 없어서 GDB는 <optimized out>이라고 답할 수밖에 없습니다. GDB의 버그가 아닙니다. 해결법:

# -O0으로 재빌드
g++ -g -O0 main.cpp -o myapp
# 또는 디버깅 친화적인 최적화 (-O0보다 빠르고, 대부분의 변수가 보임)
g++ -g -Og main.cpp -o myapp

최적화 빌드에서만 재현되는 버그라면 -O0으로 바꾸는 순간 버그가 사라지기도 합니다. 이럴 때는 -O2 -g를 유지한 채 info registers, disassemble로 레지스터를 직접 보거나, 문제 변수만 volatile로 임시 선언해 메모리에 남도록 하는 방법을 씁니다.

GDB가 “run” 후 즉시 종료

원인: 프로그램이 정상 종료되거나, 자식 프로세스에서 크래시. 해결법:

# 자식 프로세스 추적
(gdb) set follow-fork-mode child
# fork 전에 브레이크포인트
(gdb) break fork
(gdb) run

워치포인트 “Cannot watch constant value”

원인: 상수나 레지스터만 있는 값은 워치 불가. 해결법:

# 메모리에 있는 변수에만 워치 설정
# 지역 변수는 해당 스코프에서 설정
(gdb) break main
(gdb) run
(gdb) watch arr[5]

“Program received signal SIGSEGV” — 원인 불명

해결법:

(gdb) backtrace full
(gdb) frame 0
(gdb) info locals
(gdb) info args
# 크래시 직전 상태로 run 후 break로 그 함수에 멈추고 next로 한 줄씩 진행

“ptrace: Operation not permitted” (실행 중인 프로세스에 attach 실패)

증상: gdb -p 12345로 이미 떠 있는 서버 프로세스에 붙으려는데, 같은 사용자 권한인데도 거부됩니다. 원인: Ubuntu 등 여러 배포판은 Yama 보안 모듈로 kernel.yama.ptrace_scope=1을 기본값으로 둡니다. 이 설정에서는 자기 자식 프로세스에만 ptrace할 수 있어서, gdb ./myapp으로 직접 띄운 프로그램은 되는데 이미 실행 중인 프로세스에는 붙지 못합니다. 컨테이너 안이라면 Docker가 기본으로 CAP_SYS_PTRACE 권한을 주지 않는 것이 원인입니다. 해결법:

cat /proc/sys/kernel/yama/ptrace_scope            # 1이면 자식만 허용
sudo gdb -p 12345                                 # 가장 간단한 방법
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope   # 임시 완화 (재부팅 시 원복)
docker run --cap-add=SYS_PTRACE ...               # 컨테이너 안에서 디버깅할 때

“로컬에서는 되는데 컨테이너에서만 안 된다”는 질문의 대부분이 이 경우입니다. ptrace_scope를 0으로 바꾸면 같은 사용자의 어떤 프로세스든 다른 프로세스의 메모리를 읽을 수 있게 되므로, 공용 서버에서는 영구 설정하지 말고 sudo를 쓰는 편이 안전합니다.

에러 요약표

에러원인해결
No symbol table-g 없음g++ -g -O0
Cannot access 0x0Null 포인터backtrace, print ptr
optimized out-O2 이상-O0 / -Og 재빌드
run 후 즉시 종료fork 등follow-fork-mode child
워치 불가상수/레지스터메모리 변수에만
ptrace: Operation not permittedYama ptrace_scope, 컨테이너 권한sudo, —cap-add=SYS_PTRACE

GDB 디버깅 원칙

핵심 원칙

  1. 디버그 빌드: g++ -g -O0 -Wall main.cpp -o myapp
  2. 조건부 브레이크: break main.cpp:10 if i == 500 — 1000번 루프에서 500번째만 확인
  3. backtrace full: 크래시 시 모든 프레임의 지역 변수 확인
  4. .gdbinit: set pagination off, set print pretty on, set print array on
  5. 로그 저장: set logging file debug.log → set logging on → 디버깅 → set logging off

디버깅 전에 확인할 것

- [ ] -g -O0로 빌드했는가?
- [ ] backtrace로 크래시 위치 확인
- [ ] frame N으로 해당 프레임 이동
- [ ] info locals, info args로 변수 확인
- [ ] 조건부 브레이크로 특정 케이스만 추적
- [ ] 워치포인트로 메모리 변경 추적

Core Dump·gdbserver·배치 모드로 운영 환경 디버깅

Core Dump 분석

프로덕션에서 크래시 시 core dump를 저장해 나중에 분석합니다.

# core dump 활성화 (Linux)
ulimit -c unlimited
echo /tmp/core.%e.%p | sudo tee /proc/sys/kernel/core_pattern
# 크래시 후
$ gdb ./myapp /tmp/core.myapp.12345
(gdb) backtrace
(gdb) backtrace full

디버그 심볼 분리

Release 빌드와 디버그 심볼을 분리해 배포합니다.

# Release 빌드 + 별도 .debug 파일
objcopy --only-keep-debug myapp myapp.debug
strip -g myapp
objcopy --add-gnu-debuglink=myapp.debug myapp
# 분석 시
gdb -s myapp.debug -e myapp -c core.12345

원격 디버깅 (gdbserver)

# 대상 머신
gdbserver :1234 ./myapp
# 개발 머신
gdb ./myapp
(gdb) target remote 192.168.1.100:1234
(gdb) continue

배치 모드로 자동 분석

# analyze.gdb
set pagination off
run
backtrace full
quit
# 실행
gdb -batch -x analyze.gdb ./myapp

프로덕션 디버깅 시 주의사항

flowchart TD
    A[프로덕션 크래시] --> B{디버그 빌드 있음?}
    B -->|예| C[core dump 수집]
    B -->|아니오| D[재현 환경 구축]
    C --> E[gdbserver 또는 로컬 분석]
    D --> E
    E --> F[backtrace 분석]
    F --> G[원인 파악]
    G --> H[수정 및 배포]

주의사항:

  • 프로덕션 바이너리와 core dump의 빌드가 정확히 일치해야 함. 같은 커밋에서 다시 빌드해도 경로·타임스탬프·컴파일러 버전이 다르면 주소가 어긋나 백트레이스가 엉뚱한 함수를 가리킵니다. 링커가 넣는 빌드 ID(readelf -n myapp | grep "Build ID", file myapp에도 표시)가 코어를 만든 바이너리와 같은지 먼저 확인하고, 릴리스 때마다 빌드 ID별로 디버그 심볼(myapp.debug)을 보관해 두는 것이 사후 분석의 전제 조건입니다
  • 코어 덤프에는 프로세스 메모리가 통째로 들어가므로 토큰·비밀번호·개인정보가 포함될 수 있음 → 저장 위치 권한과 보존 기간을 정해 둘 것
  • -O2 빌드에서는 변수/줄 번호가 어긋날 수 있음 → RelWithDebInfo 권장
  • gdbserver는 프로세스를 중단하므로 트래픽 적은 시간에 사용

GDB 명령어 치트시트

기능명령
실행run, run arg1 arg2, kill
브레이크포인트break, info breakpoints, delete 1
워치포인트watch variable
실행 제어next(n), step(s), finish, continue(c)
검사print(p), backtrace(bt), backtrace full, frame N, info locals, info args
타입whatis, ptype
스레드info threads, thread N, thread apply all bt
되감기record, reverse-next, reverse-continue
화면gdb -tui, layout src, Ctrl+X A
기타list(l), quit(q)

GDB 정리

도구용도
break특정 위치에서 실행 중단
watch변수 변경 시 중단
backtrace호출 스택 확인
print변수 값 출력
step/next단계별 실행
continue다음 브레이크포인트까지

핵심 원칙:

  1. printf 대신 GDB
  2. 브레이크포인트 + 조건부 브레이크
  3. backtrace로 호출 경로 파악
  4. print로 변수 검증
  5. 워치포인트로 메모리 오염 추적
  6. 프로덕션은 core dump + 심볼 분리

자주 묻는 질문 (FAQ)

Q. printf 디버깅 대신 GDB를 써야 하는 때는 언제인가요?

A. 세그폴트·크래시 원인 파악, 무한 루프, 특정 조건에서만 발생하는 버그, “누가 이 값을 덮어썼는지” 같은 메모리 오염 추적처럼 출력문을 넣고 재빌드하는 사이클이 길어지는 경우입니다. 특히 크래시는 std::cout 출력이 버퍼에 남은 채 프로세스가 죽어서 마지막 로그가 실제 크래시 위치보다 앞쪽을 가리키기 쉬운데, GDB는 멈춘 그 줄과 호출 스택을 그대로 보여줍니다.

Q. GDB와 LLDB의 차이는?

A. GDB는 Linux에서, LLDB는 macOS에서 주로 사용합니다. 명령어가 유사하므로 GDB를 익히면 LLDB도 쉽게 사용할 수 있습니다. 자세한 내용은 GDB/LLDB 디버거 가이드를 참고하세요.

Q. 프로덕션에서 GDB를 붙여도 되나요?

A. gdbserver로 붙이면 프로세스가 중단되므로, 가능하면 core dump를 수집해 오프라인에서 분석하는 것이 좋습니다. 트래픽이 적은 시간에만 gdbserver 사용을 권장합니다. GDB 브레이크포인트·워치포인트·백트레이스·print·step/next로 버그를 빠르게 찾을 수 있습니다. 다음으로 GDB/LLDB 디버거 가이드(#16-1)를 읽어보면 좋습니다. 이전 글: CMake 입문

같이 보면 좋은 글