본문으로 건너뛰기
Previous
Next
C++ GDB | '디버거' 가이드

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

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

이 글의 핵심

C++ GDB로 중단점·스텝 실행·변수 검사·백트레이스를 다룹니다. ptrace·DWARF·심볼·원격 디버깅 프로토콜·프로덕션 패턴까지 내부와 실무를 함께 정리합니다.

들어가며

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: 기본 디버깅

factorial 함수의 구현 예제입니다.

// 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: 최적화로 인한 변수 제거

main 함수의 구현 예제입니다.

#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스레드 전환

9. ptrace와 커널 인터페이스

유저 공간에서 동작하는 GDB는 자체적으로 프로세스를 “정지”시킬 권한이 없습니다. 대신 POSIX 계열 Linux에서는 ptrace(2) 계열 시스템 콜을 통해 커널이 추적자(tracer)와 피추적자(tracee) 관계를 맺고, 신호·실행·메모리·레지스터 접근을 중재합니다. GDB가 “한 줄씩 실행”하거나 “중단점에서 멈춘다”는 체감은 대부분 이 경로 위에서 구현됩니다.

추적 세션 수립: TRACEME와 ATTACH

전형적인 로컬 디버깅은 두 가지 경로 중 하나로 시작합니다.

  • PTRACE_TRACEME: 디버깅 대상 프로세스가 스스로 추적을 허용합니다. gdb ./a.out(gdb) run으로 자식을 띄우는 패턴에서, 런타임이 ptrace(PTRACE_TRACEME, …)에 해당하는 초기화를 거치며 부모(GDB)와 연결됩니다(구체적인 호출 순서는 런타임·GDB 버전에 따라 다르지만, 결과적으로 부모가 자식의 디버그 이벤트를 받는 구조가 됩니다).
  • PTRACE_ATTACH / PTRACE_SEIZE: 이미 실행 중인 PID에 붙습니다. gdb -p <pid> 같은 흐름에서 사용되며, 대상은 SIGSTOP 등으로 멈춘 뒤 디버거가 제어권을 가집니다. 프로덕션에서는 서비스 중단·락 경합·시그널 처리에 영향을 주므로 매우 신중해야 합니다.

실행 재개, 단일 스텝, 신호 전달

추적자는 대기 상태에서 waitpid(2)류로 이벤트를 받습니다. tracee가 중단점·트랩·신호로 멈추면 GDB는 사용자 명령에 따라 다음을 수행합니다.

  • PTRACE_CONT: 지정한 신호(또는 0)와 함께 실행을 재개합니다. “계속 실행”의 저수준 대응입니다.
  • PTRACE_SINGLESTEP: 아키텍처별로 한 명령어만 실행합니다. x86에서는 플래그 레지스터의 트랩 플래그(TF)를 이용하는 경로와 ptrace 단일 스텝이 함께 쓰이기도 합니다.
  • PTRACE_SYSCALL: 시스템 콜 입구/출구에서 멈추는 등 strace류와 유사한 관찰이 가능합니다(도구마다 래핑 방식이 다름).

신호는 단순히 “무시”되지 않습니다. 사용자가 handle SIGPIPE nostop처럼 정책을 바꾸면, GDB는 멈출지/전달할지/무시할지ptrace 경로와 시그널 디스포지션 규칙에 맞춰 결정합니다. 멀티스레드 환경에서는 어느 스레드가 어떤 이벤트로 멈췄는지info threads, thread apply all의 근거가 됩니다.

메모리와 레지스터: PEEK/POKE와 GETREGSET

중단점 구현의 핵심은 코드 메모리 패치원자적 재개입니다.

  • 소프트웨어 중단점은 대개 대상 주소의 첫 바이트를 INT3 (0xCC)로 바꿉니다. x86-64에서 흔히 보는 방식입니다. 실행이 그 주소에 도달하면 #BP 예외로 제어가 커널을 거쳐 디버거로 돌아옵니다. GDB는 원래 바이트를 보관했다가 재개 시 복원합니다.
  • PTRACE_PEEKDATA / PTRACE_POKEDATA: 워드 단위 읽기/쓰기(구현은 iov 기반 인터페이스로 발전). 대량 메모리 덤프는 /proc/<pid>/mem 등과 조합되기도 합니다.
  • PTRACE_GETREGSET / PTRACE_SETREGSET(NT_X86_64 등): 범용 레지스터·벡터 레지스터·FP 상태를 다룹니다. 백트레이스·인자 복원·코어덤프 일치 여부가 여기에 좌우됩니다.

하드웨어 워치포인트와 디버그 레지스터

watch메모리 쓰기/읽기/실행을 감시할 때, CPU가 지원하면 디버그 레지스터(DR0–DR7 등) 기반의 하드웨어 브레이크포인트를 씁니다. 개수는 CPU·모드에 따라 제한적이며, 초과 시 GDB는 소프트웨어 방식으로 떨어지거나 실패할 수 있습니다. 임베디드·커널 디버깅에서 흔히 겪는 “워치포인트 슬롯 부족”의 근본 원인입니다.

보안·권한·컨테이너

ptrace강력한 프로세스 간 침입 채널이므로 yama.ptrace_scope, capabilities(CAP_SYS_PTRACE), Docker --cap-add=SYS_PTRACE, --security-opt seccomp=unconfined 같은 정책과 충돌합니다. “컨테이너 안에서만 안 붙는다”는 문제의 상당수는 정책이 ptrace를 막아서입니다.


10. DWARF 디버그 정보 포맷

-g로 넣는 디버그 정보의 사실상 표준은 DWARF(대개 ELF의 .debug_* 섹션)입니다. GDB가 list로 소스를 보여주고 print로 구조체 멤버를 풀어내는 이유는 실행 코드와 소스·타입 사이에 DWARF가 “맵”을 제공하기 때문입니다.

섹션 지도: 무엇이 어디에 있나

대표적인 ELF 내 DWARF 섹션은 다음과 연관됩니다(툴체인·링커에 따라 통합·압축 형태가 다를 수 있음).

섹션(개념)역할
.debug_infoDIE 트리 본체: 함수·변수·타입·템플릿 인스턴스 등의 노드와 속성
.debug_abbrevDIE 형식을 압축 기술하는 약어 테이블
.debug_str / .debug_line_str문자열 풀(파일 경로·이름 등)
.debug_line라인 번호 프로그램: 기계어 주소 ↔ 소스 파일·라인·컬럼 매핑
.debug_loc / .debug_loclists위치 리스트: 최적화로 변수가 레지스터/스택 슬롯 사이를 옮겨 다닐 때 어느 PC 구간에서 어디에 살아 있는지
.debug_frame / .eh_frameCFA·프레임 정보: 백트레이스 복원, -fomit-frame-pointer 환경에서 특히 중요
.debug_ranges / .debug_rnglists비연속 범위(인라인·아웃라인 코드 조각)

DIE: 타입과 스코프의 그래프

DWARF는 DIE(Debug Information Entry)의 계층으로 표현됩니다. DW_TAG_subprogram, DW_TAG_variable, DW_TAG_class_type 같은 태그가 노드 종류이고, DW_AT_name, DW_AT_low_pc, DW_AT_location 등이 속성입니다. C++ 템플릿·네임스페이스·람다 클로저처럼 복잡한 스코프도 이 그래프로 풀립니다.

실무에서 중요한 함정 하나는 최적화(-O2 이상)입니다. 변수가 “논리적으로는 존재”해돀로 프로모션·복사 제거·인라인으로 DIE 상에서 사라지거나 .debug_loc이 복잡해져 <optimized out>이 뜹니다. 이는 GDB 버그라기보다 디버그 정보가 표현할 수 있는 실체가 런타임에 없어지는 경우가 많습니다.

라인 테이블과 “한 줄 스텝”의 의미

.debug_line은 단순한 배열이 아니라 상태 머신 기반 라인 프로그램입니다. 컴파일러가 기계어 블록을 재배치하면, 소스 한 줄이 여러 주소 범위에 걸치거나, 한 주소가 여러 라인 후보와 연결될 수 있습니다. 그래서 동일한 next라도 최적화·인라인 때문에 “소스 상 한 줄”과 “기계어 상 한 스텝”이 어긋날 수 있습니다.

GCC/Clang 옵션과 DWARF 버전

-g, -g3, -gdwarf-4, -gdwarf-5 등은 포함 정보의 풍부함과 DWARF 버전을 바꿉니다. 매크로 디버깅·더 풍부한 타입 정보는 바이너리와 빌드 시간을 늘립니다. 팀 빌드 시스템에서는 동일한 툴체인·동일 DWARF 세대를 맞추는 것이, 코어 파일과 별도 디버그 심볼을 합칠 때 스트레스를 줄입니다.

유틸로 DWARF 엿보기

개념 검증에는 다음이 유용합니다.

# DWARF 덤프(양 많음)
readelf -w ./a.out | less

# CU(Compilation Unit) 단위 요약 등
llvm-dwarfdump --debug-info ./a.out | less

11. 심볼 테이블과 네임 맹글링

심볼은 “이름”만이 아니라 링크·동적 로딩·디버깅 각각에 다른 표가 있습니다.

ELF에서의 정적·동적 심볼

  • .symtab: 링크 타임 심볼(전역·정적·지역 심볼이 포함되는 경우가 많음). strip으로 제거되기 쉽습니다.
  • .dynsym: 동적 링커가 필요로 하는 동적 심볼. strip 후에도 일부 이름이 남을 수 있으나, 디버깅에 충분하지 않은 경우가 많습니다.
  • .strtab / .dynstr: 심볼 이름 문자열 테이블.

nm, readelf -s, objdump -t로 어떤 이름이 남았는지 확인할 수 있습니다. “릴리스 바이너리에 함수 이름이 보인다”는 현상은 동적 심볼·내보내기 정책 때문인 경우가 많습니다.

C++ 이타늄 ABI 맹글링

C++는 오버로드·네임스페이스·템플릿 때문에 링커 수준 이름이 맹글링됩니다(GCC/Clang: Itanium C++ ABI). 예를 들어 foo(int)foo(double)는 서로 다른 심볼 문자열이 됩니다. GDB는 기본적으로 출력 시 디맹글을 시도합니다.

# 맹글된 원시 심볼 확인(nm 출력 등) 후
c++filt _ZN3foo3barEi

# GDB 내부
(gdb) set print asm-demangle on

템플릿 인스턴스가 수만 개인 코드베이스에서는 심볼 문자열 자체가 매우 길고, 디버그 세션 검색·탭 완성이 느려질 수 있습니다.

.gnu_debuglink와 빌드 ID

운영 바이너리에서 디버그 정보를 떼어낼 때 흔한 패턴은:

  1. objcopy --only-keep-debug전용 디버그 파일 생성
  2. 본 바이너리는 strip
  3. objcopy --add-gnu-debuglink링크 또는 빌드 ID 기반 분리 디버그 패키지 배포

GDB는 .gnu_debuglink의 CRC와 빌트인 빌드 ID(--build-id)를 이용해 코어 파일·바이너리·별도 디버그 정보의 대응 관계를 검증합니다. “같은 소스인데 스택이 안 맞는다”는 경우, 가장 먼저 의심할 것은 정확히 동일 빌드 산출물이 아닌 디버그 심볼입니다.


12. 원격 디버깅 프로토콜(GDB RSP)

원격 디버깅은 보통 gdbserver(대상 머신) + GDB(호스트) 조합입니다. 둘 사이의 대화 규약이 GDB Remote Serial Protocol(RSP) 입니다. 전송 계층은 TCP, UDP, 시리얼, 파이프 모두 가능하지만 텍스트 기반 패킷이라는 점은 동일합니다.

패킷 형식과 역할 분담

RSP 메시지는 대개 $패이로드#CS 형태이며 CS는 체크섬입니다. gdbserver대상 프로세스에 실제로 ptrace/플랫폼별 API를 적용하고, GDB는 UI·심볼·소스를 담당합니다. 그래서 심볼이 없는 임베디드 타깃이라도 중단·스텝·레지스터 덤프는 동작할 수 있으나, 소스 레벨 스텝은 사이드에 ELF+DWARF가 있을 때 비로소 빛납니다.

시작 예: 포트 리스닝

# 대상 장치/서버에서
gdbserver :1234 ./a.out

# 개발 머신 GDB에서
(gdb) file ./a.out
(gdb) target remote host:1234

멀티 프로세스·멀티 인ferior 설정은 GDB 최신 버전에서 확장되어 왔으며, “원격이 n개 프로세스” 같은 시나리오는 maint info remote-protocol류로 디버깅하기도 합니다.

스레드·불연속 메모리·확장 패킷

현대 GDB는 RSP 위에 스레드 식별자(Hg, vCont 등), 불연속 메모리 전송(x/X 변형, vFile 계열), qXfer 전송(threads·libraries XML 등)을 올립니다. 구현체가 구버전이면 멀티스레드 단일 스텝이 어색하거나 특정 기능이 빠질 수 있습니다.

원격 경로에서는 레이턴시가 곧 비용입니다. 대형 바이너리에서 섹션 로딩·심볼 인덱싱이 느리면, GDB 쪽에서 set pagination off, 인덱스 캐시 생성, 불필요한 자동 solib 스캔 줄이기 같은 운영 팁이 필요합니다.


13. 프로덕션 디버깅 패턴

개발용 대화형 세션과 달리, 프로덕션에서는 가용성·보안·규제가 우선입니다. 아래는 “GDB를 쓴다”는 관점의 성숙한 패턴입니다.

사후(post-mortem) 우선: 코어와 대응 심볼

가장 안전한 1순위는 크래시 시 코어 덤프 확보 + 빌드 산출물과 짝이 맞는 디버그 정보입니다. systemd-coredump, kernel.core_pattern, 컨테이너 볼륨 마운트 등으로 코어가 어디에 쌓이는지를 사전에 고정하고, 릴리스 파이프라인에서 빌드 ID → 디버그 아카이브 매핑을 자동화합니다. GDB는 여기서 bt, thread apply all bt, info registers, 필요 시 frame 단위 print로 원인을 좁힙니다.

라이브 attach는 최후의 수단

gdb -p로 살아 있는 PID에 붙는 것은 지연 스파이크·락 보유 스레드 정지·시그널 마스킹 상호작용을 유발할 수 있습니다. 반드시 필요하면 트래픽 드레인·읽기 전용 복제본·카나리 인스턴스에서 수행하고, 짧은 시간·명확한 목적(스택 샘플 몇 장)으로 제한합니다.

배치 모드와 재현 가능한 출력

자동화·CI에서는 대화형 대신 gdb -batch -ex 'thread apply all bt' -ex quit --args ./a.out core처럼 배치 커맨드 체인을 씁니다. 운영 장애 대응용 스크립트는 출력 고정(로케일·정렬·페이지네이션 끔)을 함께 두면 diff와 공유가 쉬워집니다.

“관측 가능성”이 디버거를 대체하는 지점

프로덕션의 대부분 이슈는 분산 추적·구조화 로그·프로파일러(eBPF, perf, 동적 추적)가 먼저 답합니다. GDB는 특정 재현이 있고 메모리/스레드 상태가 핵심인 경우에 강합니다. 역으로 Heisenbug류는 최적화·타이밍에 민감하므로, 동일 바이너리에 ASan/UBSan 빌드를 별도 파이프로 돌리는 전략이 더 나을 때가 많습니다.

보안·비밀·컴플라이언스

코어 덤프에는 메모리 전체가 포함될 수 있어 토큰·키·PII가 섞입니다. 저장소 권한·암호화·보존 기간·접근 로그는 운영 정책의 일부여야 합니다. 원격 gdbserver를 열어두는 것은 방화벽·인증·TLS 터널 없이는 금기에 가깝습니다.

최소 체크리스트

  1. 코어 수집 경로·크기 제한·회전이 정의되어 있는가
  2. 바이너리 빌드 ID ↔ 디버그 아카이브가 자동 매칭되는가
  3. strip 정책이 배포 아티팩트와 충돌하지 않는가(분리 디버그 링크 포함)
  4. 컨테이너에서 ptrace 허용이 필요한 작업만 최소 권한으로 열려 있는가
  5. 장애 시 대화형 GDB 대신 배치 백트레이스 → 소스 레벨 분석 순서가 문서화되어 있는가

정리

핵심 요약

  1. GDB: C/C++ 디버깅 도구
  2. -g 플래그: 디버그 정보 포함 필수
  3. 중단점: break 명령으로 설정
  4. 변수 검사: print, display로 확인
  5. 스택 추적: backtrace로 호출 스택 확인
  6. 조건부 중단: break ....if 조건 지정
  7. 워치포인트: watch로 변수 변경 감시
  8. 코어 덤프: 크래시 원인 분석
  9. ptrace: 커널이 추적·중단·메모리/레지스터 접근을 중재하는 저수준 인터페이스
  10. DWARF: 소스·타입·라인·위치 리스트를 담는 디버그 정보 포맷(.debug_*)
  11. 심볼·맹글링: ELF·ABI·.gnu_debuglink/빌드 ID로 본 바이너리와 디버그 정보를 짝지음
  12. RSP: gdbserver와 GDB가 오가는 원격 직렬 프로토콜
  13. 프로덕션: 코어+분리 심볼 우선, 라이브 attach는 최소화, 배치·보안·관측 가능성과 병행

실전 팁

컴파일:

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

디버깅:

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

효율:

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

다음 단계


관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ GDB | ‘디버거’ 가이드」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ GDB | ‘디버거’ 가이드」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


자주 묻는 질문 (FAQ)

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

A. GDB로 C++ 디버깅하는 실전 가이드. 브레이크포인트·watchpoint·스택 추적·멀티스레드 디버깅까지. 코어 덤프 분석과 IDE 통합, 프로덕션 환경 원격 디버깅 전략 포함. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

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

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

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


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

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


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

C++, gdb, debugging, tools, breakpoint, ptrace, DWARF, 원격디버깅 등으로 검색하시면 이 글이 도움이 됩니다.