[2026] C++ 디버깅 기초 완벽 가이드 | GDB·LLDB 브레이크포인트·워치포인트로 버그 5분 만에 찾기

[2026] C++ 디버깅 기초 완벽 가이드 | GDB·LLDB 브레이크포인트·워치포인트로 버그 5분 만에 찾기

이 글의 핵심

C++ 디버깅 기초 : GDB·LLDB 브레이크포인트·워치포인트로 버그 5분 만에 찾기. printf 디버깅의 한계·문제 시나리오.

들어가며: printf 디버깅의 한계

”cout 100개를 찍어도 버그를 못 찾겠어요”

세그폴트가 발생하는 버그를 찾고 있었습니다. std::cout수십 개 추가했지만 원인을 찾지 못했습니다. 출력이 버퍼링되어 정확한 크래시 위치를 알 수 없었고, 재컴파일·재실행을 반복하는 데 시간이 많이 들었습니다. 디버거는 “어느 줄에서 멈췄는지, 그때 변수 값과 스택이 어떤지”를 멈춘 상태에서 직접 볼 수 있게 해 줍니다. 브레이크포인트(실행을 멈출 지점)·워치포인트(특정 변수 변경 시 자동 중단)·조건부 중단·백트레이스(호출 스택)만 익혀도 printf 디버깅보다 훨씬 빠르게 버그 위치를 좁힐 수 있습니다. 비유하면: printf 디버깅은 “사진 한 장씩 찍어서 사고 현장을 추측하는 것”이고, 디버거는 “시간을 멈추고 그 순간의 모든 상태를 직접 들여다보는 것”입니다. 요구 환경: GDB(Linux/WSL: apt install gdb) 또는 LLDB(macOS: xcode-select --install). 빌드 시 -g 옵션 필수. 이 글을 읽으면:

  • GDB/LLDB의 핵심 명령어를 실전에서 사용할 수 있습니다.
  • 브레이크포인트·워치포인트를 상황에 맞게 설정할 수 있습니다.
  • 변수·메모리·스택을 검사하고 원인을 추적할 수 있습니다.
  • 프로덕션 환경에서의 디버깅 패턴을 적용할 수 있습니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  1. 문제 시나리오
  2. 디버거 시작하기
  3. 브레이크포인트 완전 가이드
  4. 워치포인트로 메모리 추적
  5. 단계별 실행
  6. 변수·메모리 검사
  7. GDB/LLDB 완전 예제
  8. 자주 발생하는 에러와 해결법
  9. 디버깅 모범 사례
  10. 프로덕션 디버깅 패턴

1. 문제 시나리오

시나리오 1: “세그폴트가 나는데 어디서 터지는지 모르겠어요”

"프로그램이 Segmentation fault로 죽어요. 로그에는 아무것도 안 나와요."
"어느 함수에서 문제인지 전혀 감이 안 잡혀요."

상황: 배열 인덱스 범위 초과, null 포인터 역참조 등으로 크래시합니다. printf를 여러 곳에 넣었지만 출력이 버퍼링되거나, 크래시 직전 로그가 안 찍혀서 위치를 좁히지 못합니다. 해결 포인트: GDB/LLDB로 실행 → 크래시 시 backtrace로 호출 스택 확인 → frame N으로 해당 프레임 이동 → print 변수로 원인 파악.

시나리오 2: “1000번 루프 중 500번째에서만 버그가 나요”

"대부분은 정상인데, 가끔 잘못된 결과가 나와요."
"조건을 재현하기 어려워서 printf로 찾기 힘들어요."

상황: 특정 반복 횟수, 특정 입력에서만 버그가 발생합니다. 매번 1000번 출력하는 것은 비효율적이고, 조건부 printf는 코드를 지저분하게 만듭니다. 해결 포인트: 조건부 브레이크포인트 (break main.cpp:20 if i == 500)로 해당 조건에서만 중단.

시나리오 3: “변수 값이 어디선가 바뀌는데 찾을 수 없어요”

"초기화한 변수가 나중에 이상한 값으로 바뀌어 있어요."
"어디서 덮어쓰는지 전혀 모르겠어요."

상황: 메모리 오염, 버퍼 오버런, 잘못된 포인터로 인해 변수 값이 예기치 않게 변경됩니다. 수백 개의 함수 중 어디서 변경되는지 추적하기 어렵습니다. 해결 포인트: 워치포인트 (watch variable_name)로 해당 변수 변경 시 자동 중단.

시나리오 4: “무한 루프에 빠졌어요”

"프로그램이 멈춘 것처럼 보여요. Ctrl+C로 중단했는데 원인을 모르겠어요."

상황: while 조건 오류, i++ 누락 등으로 무한 루프에 빠집니다. printf를 넣으면 출력이 너무 많아서 의미 있는 정보를 얻기 어렵습니다. 해결 포인트: Ctrl+C로 중단 → backtrace로 현재 위치 확인 → print 루프 변수로 값 확인.

시나리오 5: “멀티스레드에서 데드락이 발생해요”

"두 스레드가 서로를 기다리다 멈춘 것 같아요."
"어느 스레드가 어느 뮤텍스를 잡고 있는지 모르겠어요."

상황: 뮤텍스 잠금 순서 불일치로 데드락 발생. 단일 스레드에서는 재현되지 않습니다. 해결 포인트: Ctrl+C로 중단 → thread apply all backtrace로 모든 스레드의 호출 스택 확인.

시나리오 6: “프로덕션에서만 가끔 크래시해요”

"개발 PC에서는 절대 안 나는데, 서버에서 가끔 죽어요."
"core dump가 없어서 분석을 못 해요."

상황: 특정 부하, 특정 데이터에서만 발생하는 버그. 재현이 어렵고, 프로덕션에서는 디버거를 직접 붙이기 어렵습니다. 해결 포인트: core dump 활성화 → 크래시 시 core 파일 수집 → GDB로 core 파일 분석.

문제 시나리오별 도구 선택

아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

flowchart TD
    A[버그 발생] --> B{유형?}
    B -->|크래시| C[GDB/LLDB run → backtrace]
    B -->|조건부 버그| D[조건부 브레이크포인트]
    B -->|변수 오염| E[워치포인트]
    B -->|무한 루프| F[Ctrl+C → backtrace]
    B -->|데드락| G[thread apply all bt]
    B -->|프로덕션| H[core dump 분석]

2. 디버거 시작하기

디버그 빌드 (필수)

-g 옵션을 주면 실행 파일에 디버그 심볼(소스 줄 번호, 변수 이름)이 들어갑니다. -O0로 최적화를 끄면 변수가 최적화로 사라지거나 코드 순서가 바뀌는 일이 줄어듭니다. 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

GDB 시작

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 프로그램 로드
gdb ./myapp
# 실행
(gdb) run
# 인자와 함께 실행
(gdb) run arg1 arg2
# 환경 변수 설정 후 실행
(gdb) set env LD_LIBRARY_PATH=/path/to/libs
(gdb) run
# 종료
(gdb) quit

주의사항: PIE 바이너리는 로드 주소가 달라질 수 있어, ASLR을 끄는 옵션은 보안상 개발 전용으로만 쓰세요.

LLDB 시작 (macOS)

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 프로그램 로드
lldb ./myapp
# 실행
(lldb) run
# 인자와 함께 실행
(lldb) run arg1 arg2
# 종료
(lldb) quit

디버깅 워크플로우

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

sequenceDiagram
    participant Dev as 개발자
    participant GDB as GDB/LLDB
    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: 다음 브레이크포인트까지 실행

3. 브레이크포인트 완전 가이드

브레이크포인트란?

실행을 특정 위치에서 멈추게 하는 지점입니다. 멈춘 상태에서 변수 값, 호출 스택, 메모리를 검사할 수 있습니다.

GDB 브레이크포인트 명령어

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 함수에 브레이크포인트
(gdb) break main
(gdb) break processData
(gdb) b main  # 단축
# 파일:라인에 브레이크포인트
(gdb) break main.cpp:15
(gdb) b main.cpp:15
# 조건부 브레이크포인트 (i가 50일 때만 멈춤)
(gdb) break main.cpp:20 if i == 50
# 포인터가 null일 때만
(gdb) break main.cpp:25 if ptr == nullptr
# 브레이크포인트 목록
(gdb) info breakpoints
(gdb) i b
# 브레이크포인트 삭제
(gdb) delete 1        # 번호로 삭제
(gdb) delete          # 모두 삭제
(gdb) clear main.cpp:15  # 위치로 삭제
# 브레이크포인트 비활성화/활성화
(gdb) disable 1
(gdb) enable 1
# 일시 중단 횟수 설정 (N번째 도달 시에만 멈춤)
(gdb) ignore 1 99    # 1번 브레이크포인트를 99번 무시 후 멈춤

LLDB 브레이크포인트 명령어

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 함수에 브레이크포인트
(lldb) breakpoint set --name main
(lldb) b main  # 단축
# 파일:라인
(lldb) breakpoint set --file main.cpp --line 15
(lldb) b main.cpp:15
# 조건부
(lldb) breakpoint set --name processData --condition 'i == 50'
(lldb) breakpoint set -f main.cpp -l 20 -c 'ptr == nullptr'
# 목록
(lldb) breakpoint list
(lldb) br list
# 삭제
(lldb) breakpoint delete 1
(lldb) breakpoint delete  # 모두 삭제
# 비활성화/활성화
(lldb) breakpoint disable 1
(lldb) breakpoint enable 1

조건부 브레이크포인트 활용 예

다음은 간단한 cpp 코드 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// conditional_bug.cpp - 1000번 중 500번째에서만 버그
for (int i = 0; i < 1000; ++i) {
    process(i);  // i=500일 때만 잘못된 동작
}

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# GDB: i가 500일 때만 main.cpp 10번 줄에서 멈춤
(gdb) break main.cpp:10 if i == 500
(gdb) run
# LLDB
(lldb) breakpoint set -f main.cpp -l 10 -c 'i == 500'
(lldb) run

4. 워치포인트로 메모리 추적

워치포인트란?

특정 변수나 메모리 주소가 변경될 때 자동으로 실행을 중단하는 기능입니다. “어디서 이 값이 바뀌는가?”를 찾을 때 유용합니다.

GDB 워치포인트

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 변수 변경 시 멈춤 (쓰기)
(gdb) watch variable_name
# 포인터가 가리키는 값 변경 시
(gdb) watch *ptr
# 읽기 시 멈춤 (rwatch)
(gdb) rwatch variable_name
# 읽기 또는 쓰기 시 멈춤 (awatch)
(gdb) awatch variable_name
# 워치포인트는 break main 등으로 프로그램 시작 후 설정
(gdb) break main
(gdb) run
(gdb) watch arr[5]   # 이제 arr[5]가 스코프에 있음
(gdb) continue

LLDB 워치포인트

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 변수 변경 시 멈춤
(lldb) watchpoint set variable variable_name
(lldb) w s v variable_name  # 단축
# 표현식(포인터 역참조) 감시
(lldb) watchpoint set expression -- ptr
(lldb) watchpoint set expression -w write -- *(int*)0x7fff1234
# 목록
(lldb) watchpoint list
# 삭제
(lldb) watchpoint delete 1

워치포인트 실전 예: 메모리 오염 찾기

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// memory_corruption.cpp
#include <iostream>
int global_counter = 0;
void suspiciousFunction() {
    int buffer[10] = {0};
    // 버퍼 오버런: buffer[10]에 쓰면 global_counter를 덮을 수 있음
    for (int i = 0; i <= 10; ++i) {
        buffer[i] = i;  // i=10일 때 범위 초과!
    }
}
int main() {
    std::cout << "Before: " << global_counter << "\n";
    suspiciousFunction();
    std::cout << "After: " << global_counter << "\n";  // 예상치 못한 값
    return 0;
}

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# GDB로 global_counter 변경 지점 찾기
# 실행 예제
$ g++ -g -O0 -o corrupt memory_corruption.cpp
$ gdb ./corrupt
(gdb) break main
(gdb) run
(gdb) watch global_counter
(gdb) continue
# global_counter가 변경되면 멈춤
Hardware watchpoint 2: global_counter
Old value = 0
New value = 10
suspiciousFunction () at memory_corruption.cpp:10

일상 비유로 이해하기: 메모리를 아파트 건물로 생각해보세요. 스택은 엘리베이터 같아서 빠르지만 공간이 제한적입니다. 힙은 창고처럼 넓지만 물건을 찾는 데 시간이 걸립니다. 포인터는 “3층 302호”처럼 주소를 가리키는 메모지라고 보면 됩니다.

5. 단계별 실행

next vs step

명령GDBLLDB동작
다음 줄 (함수 건너뜀)next / nnext / n현재 줄 실행 후 다음 줄로. 함수 호출 시 함수 안으로 들어가지 않음
함수 진입step / sstep / s함수 호출 시 함수 내부로 들어감
함수 끝까지finishfinish현재 함수 반환까지 실행
계속continue / ccontinue / c다음 브레이크포인트까지 실행

GDB 실행 제어

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 다음 줄 실행 (함수 안으로 들어가지 않음)
(gdb) next
(gdb) n
# 함수 안으로 들어감
(gdb) step
(gdb) s
# 현재 함수 끝까지 실행
(gdb) finish
# 계속 실행 (다음 브레이크포인트까지)
(gdb) continue
(gdb) c
# N번 next/step 실행
(gdb) next 5
(gdb) step 3
# 현재 위치 소스 코드 보기
(gdb) list
(gdb) l

LLDB 실행 제어

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

(lldb) next
(lldb) n
(lldb) step
(lldb) s
(lldb) finish
(lldb) continue
(lldb) c
(lldb) list
(lldb) l

단계별 실행 흐름

아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

6. 변수·메모리 검사

변수 출력

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# GDB
(gdb) print x
(gdb) p x
# 포인터 역참조
(gdb) print *ptr
# 배열 10개 원소
(gdb) print arr[0]@10
# 구조체
(gdb) print person
(gdb) print person.name
# STL vector (GDB 7.0+)
(gdb) print vec
(gdb) print vec.size()

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# LLDB
(lldb) frame variable
(lldb) fr v
(lldb) frame variable x
(lldb) p x
(lldb) expression ptr->member
(lldb) expr vec.size()

메모리 검사 (x 명령)

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# GDB: 메모리 내용 보기
# 형식: x/[개수][형식][크기] 주소
(gdb) x/10x ptr   # 16진수 10개 (4바이트씩)
(gdb) x/10d ptr   # 10진수 10개
(gdb) x/10s ptr   # 문자열 10개
(gdb) x/20xb ptr  # 1바이트 20개, 16진수
# 형식: x(16진), d(10진), s(문자열), i(명령어)
# 크기: b(1바이트), h(2바이트), w(4바이트), g(8바이트)

스택 추적 (백트레이스)

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# GDB
(gdb) backtrace
(gdb) bt
# 모든 프레임의 지역 변수 포함
(gdb) backtrace full
(gdb) bt full
# 특정 프레임으로 이동
(gdb) frame 2
(gdb) f 2
# 현재 프레임 정보
(gdb) info frame
# 현재 프레임의 지역 변수
(gdb) info locals
# 함수 인자
(gdb) info args

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# LLDB
(lldb) thread backtrace
(lldb) bt
(lldb) frame select 2
(lldb) f 2
(lldb) frame variable

스택 프레임 구조

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TB
    subgraph stack["호출 스택 (아래→위)"]
        F0["main() - 프레임 2"]
        F1["processData() - 프레임 1"]
        F2["buggyFunction() - 프레임 0 (현재)"]
    end
    F0 --> F1
    F1 --> F2
    subgraph vars[프레임 0 지역 변수]
        V1["i = 10"]
        V2["size = 10"]
        V3["arr = 0x7fff..."]
    end

7. GDB/LLDB 완전 예제

예제 1: 배열 범위 초과 (세그폴트)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

GDB 디버깅 과정: 다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

$ g++ -g -O0 -o buggy buggy_array.cpp
$ gdb ./buggy
(gdb) run
Program received signal SIGSEGV, Segmentation fault.
(gdb) backtrace
#0  buggyFunction (arr=0x7fffffffe2a0, size=10) at buggy_array.cpp:5
#1  main () at buggy_array.cpp:12
(gdb) frame 0
(gdb) print i
$1 = 10   # ← 버그! 유효 인덱스는 0-9
(gdb) print size
$2 = 10
(gdb) print arr[10]
Cannot access memory at address 0x...

수정: 다음은 간단한 cpp 코드 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ i < size
for (int i = 0; i < size; ++i) {
    arr[i] = i * 2;
}

예제 2: Null 포인터 역참조

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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;
}

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

(gdb) break sumList
(gdb) run
(gdb) next
(gdb) print head
$1 = (Node *) 0x0   # null 포인터!
(gdb) backtrace full

예제 3: 무한 루프

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# Ctrl+C로 중단
(gdb) run
^C
Program received signal SIGINT, Interrupt.
(gdb) backtrace
#0  processData () at main.cpp:5
(gdb) print i
$1 = 0   # 변하지 않음 → i++ 누락 발견

예제 4: 조건부 브레이크 (1000번 중 500번째만)

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

(gdb) break main.cpp:10 if i == 500
(gdb) run
# 멈춘 후
(gdb) print i
$1 = 500
(gdb) step
(gdb) backtrace

예제 5: 워치포인트로 메모리 오염

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

(gdb) break main
(gdb) run
(gdb) watch arr[5]
(gdb) continue
# arr[5] 변경 시
Hardware watchpoint 2: arr[5]
Old value = 0
New value = 999
someFunction () at memory_corruption.cpp:15

예제 6: STL vector 범위 초과

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// vector_bug.cpp
std::vector<int> vec = {1, 2, 3};
for (size_t i = 0; i <= vec.size(); ++i) {  // ❌ <= 버그
    std::cout << vec[i] << "\n";
}

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

(gdb) run
Program received signal SIGSEGV
(gdb) print i
$1 = 3
(gdb) print vec.size()
$2 = 3
# vec[3] 접근 → 범위 초과

예제 7: 멀티스레드 데드락

다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// deadlock.cpp
std::mutex m1, m2;
void thread1() { m1.lock(); /* ....*/ m2.lock(); /* ....*/ }
void thread2() { m2.lock(); /* ....*/ m1.lock(); /* ....*/ }

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# Ctrl+C 후
(gdb) thread apply all backtrace
Thread 1:
#0  __lll_lock_wait ()
#1  thread1 () at deadlock.cpp:10
Thread 2:
#0  __lll_lock_wait ()
#1  thread2 () at deadlock.cpp:20
# 두 스레드가 서로의 뮤텍스를 기다림 → 데드락 확인

예제 8: 재귀 스택 오버플로우

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
int main() {
    int result = factorial(-5);  // ❌ 음수 → 무한 재귀
    return 0;
}

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

(gdb) run
Program received signal SIGSEGV
(gdb) backtrace
#0  factorial (n=-1048576) at stack_overflow.cpp:3
#1  factorial (n=-1048575) at stack_overflow.cpp:4
...
#1048575  factorial (n=-5) at stack_overflow.cpp:4
#1048576  main () at stack_overflow.cpp:8
# n이 음수로 전달되어 종료 조건 미도달 → 스택 오버플로우

8. 자주 발생하는 에러와 해결법

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

원인: -g 옵션 없이 빌드함. 해결법: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

”Cannot access memory at address 0x0”

원인: Null 포인터 역참조. 해결법: 다음은 간단한 bash 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

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

원인: -O2, -O3 등 최적화로 변수가 레지스터에만 있거나 제거됨. 해결법: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# -O0으로 재빌드
g++ -g -O0 main.cpp -o myapp
# 또는 volatile 사용 (해당 변수만)
volatile int debug_var = value;

”Program received signal SIGSEGV” - 원인 불명

해결법: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 1. backtrace로 크래시 위치 확인
(gdb) backtrace full
# 2. 크래시 직전에 브레이크포인트 설정 후 next로 한 줄씩 진행
(gdb) break [크래시 함수]
(gdb) run
(gdb) next   # 변수 확인하며 진행

GDB가 “run” 후 즉시 종료

원인: 프로그램 정상 종료, 또는 자식 프로세스에서 크래시. 해결법: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

LLDB “variable not found”

원인: 최적화로 변수 제거, 또는 스코프 밖. 해결법:

(lldb) frame variable
(lldb) expr *(int*)0x7fff...

워치포인트 “Hardware watchpoint” 제한

원인: x86에서 하드웨어 워치포인트는 보통 4개로 제한됨. 해결법:

# 소프트웨어 워치포인트 사용 (느리지만 제한 없음)
# GDB는 자동으로 fallback. 너무 많으면 일부 삭제
(gdb) delete 2

9. 디버깅 모범 사례

1. 항상 -g -O0로 디버그 빌드

g++ -g -O0 main.cpp -o myapp

최적화가 켜져 있으면 변수가 사라지고, 줄 번호가 어긋나며, 디버깅이 어려워집니다.

2. 크래시 시 즉시 backtrace

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

(gdb) run
# SIGSEGV 수신 후
(gdb) backtrace full
(gdb) info locals
(gdb) info args

3. 조건부 브레이크포인트로 시간 절약

# 1000번 루프 중 500번째만 확인
(gdb) break main.cpp:20 if i == 500

4. 워치포인트로 메모리 오염 추적

(gdb) watch suspicious_var
(gdb) continue

5. .gdbinit 활용

다음은 간단한 bash 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# ~/.gdbinit 또는 프로젝트/.gdbinit
set pagination off
set print pretty on
set print array on

6. 로그 저장

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

(gdb) set logging file debug.log
(gdb) set logging on
(gdb) run
# ....디버깅 ...
(gdb) set logging off

디버깅 체크리스트

아래 코드는 markdown를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

10. 프로덕션 디버깅 패턴

Core Dump 분석

프로덕션에서 크래시 시 core dump를 저장해 나중에 분석합니다. 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 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

원격 디버깅 (gdbserver)

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 대상 머신 (프로덕션/임베디드)
gdbserver :1234 ./myapp
# 개발 머신
gdb ./myapp
(gdb) target remote 192.168.1.100:1234
(gdb) continue

디버그 심볼 분리

Release 빌드에서 디버그 정보를 별도 파일로 보관합니다. 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 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

프로덕션 디버깅 흐름

아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

주의사항:

  • 프로덕션 바이너리와 core dump의 빌드가 정확히 일치해야 함
  • -O2 빌드에서는 변수/줄 번호가 어긋날 수 있음 → RelWithDebInfo 권장
  • gdbserver는 프로세스를 중단하므로 트래픽 적은 시간에 사용

실무 디버깅 워크플로우

  1. 재현: 최소 입력으로 버그 재현
  2. 격리: 브레이크포인트로 범위 좁히기
  3. 가설: 변수 값으로 원인 추측
  4. 검증: 수정 후 재테스트
  5. 문서화: 원인과 해결법 기록

유용한 명령어 정리

GDB 치트시트

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 실행
run, r                    # 실행
run arg1 arg2             # 인자와 함께
kill                      # 프로그램 종료
# 브레이크포인트
break, b                  # 브레이크포인트 설정
info breakpoints          # 목록
delete 1                  # 삭제
# 실행 제어
next, n                   # 다음 줄
step, s                   # 함수 안으로
finish                    # 함수 끝까지
continue, c               # 계속
# 검사
print, p                  # 변수 출력
ptype                     # 타입 확인
backtrace, bt             # 스택 추적
info locals               # 지역 변수
info args                 # 함수 인자
# 워치포인트
watch var                 # 변수 변경 시 멈춤
# 기타
list, l                   # 소스 코드
quit, q                   # 종료

GDB vs LLDB 명령어 대응표

기능GDBLLDB
실행runrun
브레이크포인트break mainbreakpoint set -n main
조건부 브레이크break f.c:10 if i==5breakpoint set -f f.c -l 10 -c 'i==5'
다음 줄nextnext
함수 진입stepstep
변수 출력print xframe variable x
스택backtracebt
워치포인트watch varwatchpoint set variable var
종료quitquit

정리

도구플랫폼특징
GDBLinuxGNU 디버거, gdbserver 원격 지원
LLDBMac/LinuxLLVM 디버거, 빠른 성능
Visual StudioWindowsGUI 디버거
핵심 원칙:
  1. printf 대신 디버거
  2. 브레이크포인트·조건부 브레이크포인트 활용
  3. 워치포인트로 메모리 추적
  4. 스택 추적으로 원인 파악
  5. 프로덕션은 core dump + gdbserver 다음 글: [C++ 실전 가이드 #16-2] Sanitizers: 메모리 버그를 자동으로 찾는 도구

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. printf 디버깅의 한계를 넘어서. GDB·LLDB 브레이크포인트, 워치포인트, 변수 검사, 스택 추적으로 버그를 빠르게 찾는 실전 방법. 문제 시나리오부터 프로덕션 패턴까지. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다. 이전 글: [C++ 실전 가이드 #15-3] 컴파일 타임 최적화

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

이 글에서 다루는 키워드 (관련 검색어)

C++, 디버깅, GDB, LLDB, 브레이크포인트, 워치포인트, 디버거, 버그수정, 세그폴트 등으로 검색하시면 이 글이 도움이 됩니다.

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3