C++ 디버깅 기법 완벽 가이드 | GDB·LLDB·ASan·TSan·로깅 실전 [#55-8]
이 글의 핵심
C++ 디버깅: GDB/LLDB 완전 예제, AddressSanitizer·ThreadSanitizer, 구조화 로깅, 문제 시나리오, 흔한 에러, 모범 사례, 프로덕션 패턴까지 실전 코드로 다룹니다.
들어가며: “프로덕션에서 크래시가 나는데 재현이 안 돼요"
"개발 PC에서는 잘 되는데, 고객 환경에서만 Segmentation fault가 발생해요”
C++는 성능과 제어력이 뛰어나지만, 메모리 관리, 동시성, 포인터 오류로 인해 디버깅이 까다롭습니다. “내 PC에서는 절대 안 나는데”라는 말은 C++ 개발자에게 익숙한 고민입니다. 비유하면 “집에서는 잘 작동하는 자동차가 고객 차고에서는 시동이 안 걸리는 것”과 같습니다. 환경 차이, 타이밍, 메모리 레이아웃이 미묘하게 달라서 버그가 드러나거나 숨을 수 있습니다.
문제의 핵심:
- 크래시 덤프가 없거나, 있어도 심볼이 없어 분석이 어렵습니다
- 데이터 레이스는 재현이 불규칙하고, “가끔”만 발생합니다
- 메모리 오류(버퍼 오버플로우, use-after-free)는 즉시 크래시하지 않고 나중에 터질 수 있습니다
- 로그가 부족하면 “어디서” 문제가 났는지 좁히기 어렵습니다
이 글에서 다루는 것:
- 문제 시나리오: 실제 겪는 디버깅 상황
- GDB/LLDB 완전 예제: 브레이크포인트, 백트레이스, 변수 검사, 코어 덤프 분석
- AddressSanitizer(ASan): 메모리 오류 조기 발견
- ThreadSanitizer(TSan): 데이터 레이스 탐지
- 구조화 로깅: 프로덕션 추적 패턴
- 자주 발생하는 에러와 해결법
- 모범 사례와 프로덕션 패턴
요구 환경: C++17 이상, Linux/macOS (GDB/LLDB), GCC 또는 Clang
이 글을 읽으면:
- GDB/LLDB로 크래시를 분석하고 원인을 찾을 수 있습니다
- ASan/TSan으로 개발 단계에서 버그를 조기 발견할 수 있습니다
- 프로덕션 수준의 로깅과 덤프 수집 패턴을 적용할 수 있습니다
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 문제 시나리오
- GDB 완전 예제
- LLDB 완전 예제 (macOS)
- AddressSanitizer (ASan)
- ThreadSanitizer (TSan)
- 구조화 로깅
- 자주 발생하는 에러와 해결법
- 모범 사례
- 프로덕션 패턴
- 정리
1. 문제 시나리오
시나리오 1: 프로덕션에서 가끔 Segmentation fault
상황: 서버가 24시간 운영 중 가끔(하루 1~2회) 크래시합니다. 재현 조건을 알 수 없고, 크래시 시점의 로그만 있습니다.
해결: 코어 덤프를 활성화하고, GDB로 core 파일을 열어 bt full로 백트레이스를 확인합니다. 릴리즈 빌드에도 디버그 심볼을 분리해 보관하면 분석이 가능합니다.
시나리오 2: “가끔” 잘못된 값이 나와요 (데이터 레이스)
상황: 멀티스레드 앱에서 공유 변수가 가끔 이상한 값으로 바뀝니다. 단일 스레드로 실행하면 문제가 없습니다.
해결: ThreadSanitizer(TSan)로 빌드해 실행하면 데이터 레이스 발생 지점을 정확히 보고합니다. -fsanitize=thread로 컴파일합니다.
시나리오 3: 메모리 누수로 장시간 실행 후 OOM
상황: 서버가 3일째 되면 메모리 사용량이 계속 증가해 OOM으로 죽습니다. 어디서 누수가 나는지 모릅니다.
해결: LeakSanitizer(LSan, ASan에 포함)로 실행해 누수 지점을 찾습니다. 또는 Valgrind의 memcheck --leak-check=full을 사용합니다.
시나리오 4: 버퍼 오버플로우가 나중에 터짐
상황: strcpy나 배열 인덱스 오류로 인해 즉시 크래시하지 않고, 나중에 다른 함수에서 “이상한” 크래시가 납니다.
해결: AddressSanitizer로 빌드하면 오버플로우/언더플로우/use-after-free를 즉시 탐지합니다. 개발·CI에서 ASan 빌드를 돌립니다.
시나리오 5: 고객 PC에서만 크래시, 덤프 없음
상황: 고객 환경에서만 크래시하는데, 덤프 수집이 안 되어 원인 분석이 불가능합니다.
해결: 앱에 크래시 핸들러를 넣어 abort 시 자체 덤프를 파일로 저장하고, minidump 형식으로 전송받도록 합니다. 또는 Breakpad/Crashpad를 통합합니다.
디버깅 도구 선택 흐름
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph problem[문제 유형]
P1[크래시·Segfault]
P2[데이터 레이스]
P3[메모리 오류]
P4[메모리 누수]
P5[로직 버그]
end
subgraph tool[권장 도구]
T1[GDB/LLDB + 코어덤프]
T2[ThreadSanitizer]
T3[AddressSanitizer]
T4[LeakSanitizer/Valgrind]
T5[브레이크포인트·로깅]
end
P1 --> T1
P2 --> T2
P3 --> T3
P4 --> T4
P5 --> T5
2. GDB 완전 예제
환경: Linux, GCC/Clang
GDB(GNU Debugger)는 Linux에서 가장 널리 쓰이는 디버거입니다. 크래시 분석, 브레이크포인트, 변수 검사, 조건부 중단을 지원합니다.
디버그 빌드 준비
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 디버그 심볼 포함 빌드 (-g)
g++ -std=c++17 -g -O0 -o myapp main.cpp
# 릴리즈 + 분리된 디버그 심볼 (프로덕션 분석용)
g++ -std=c++17 -O2 -g -o myapp main.cpp
objcopy --only-keep-debug myapp myapp.debug
objcopy --strip-debug myapp
objcopy --add-gnu-debuglink=myapp.debug myapp
설명:
-g: 디버그 정보 포함-O0: 최적화 비활성화 (변수·라인 매핑 정확)objcopy: 릴리즈 바이너리는 심볼 제거,.debug파일로 분리해 보관
기본 GDB 명령어
다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# GDB로 프로그램 실행
gdb ./myapp
# GDB 내부 명령
(gdb) run # 실행
(gdb) run arg1 arg2 # 인자와 함께 실행
(gdb) break main # main에 브레이크포인트
(gdb) break file.cpp:42 # file.cpp 42번째 줄
(gdb) break func if x > 10 # 조건부 브레이크포인트
(gdb) continue # 다음 브레이크포인트까지 계속
(gdb) next # 다음 줄 (함수 안으로 들어가지 않음)
(gdb) step # 다음 줄 (함수 안으로 들어감)
(gdb) finish # 현재 함수 반환까지 실행
(gdb) print x # 변수 x 출력
(gdb) print *ptr # 포인터 역참조
(gdb) backtrace # 호출 스택 (bt)
(gdb) backtrace full # 스택 + 지역 변수
(gdb) frame 2 # 2번째 프레임으로 이동
(gdb) list # 소스 코드 표시
(gdb) quit # 종료
크래시 재현 및 백트레이스
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// crash_example.cpp
#include <iostream>
#include <vector>
void process(const std::vector<int>& v) {
std::cout << v[100]; // 버그: 인덱스 오버플로우
}
int main() {
std::vector<int> data = {1, 2, 3};
process(data);
return 0;
}
# 컴파일 및 실행
g++ -std=c++17 -g -O0 -o crash_example crash_example.cpp
./crash_example
# 출력: Segmentation fault (core dumped)
# 코어 덤프가 생성되었다면
gdb ./crash_example core
# 또는
gdb ./crash_example -c core
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# GDB 출력 예시
(gdb) bt full
#0 0x00007f... in std::vector<int>::operator[] (this=0x7ffc..., __n=100)
#1 0x000055... in process (v=...) at crash_example.cpp:6
#2 0x000055... in main () at crash_example.cpp:11
해석: process 함수 6번째 줄에서 v[100] 접근 시 오버플로우. v의 size()는 3이므로 인덱스 100은 유효하지 않음.
코어 덤프 활성화 (Linux)
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 현재 세션에서 무제한 크기로 코어 덤프 허용
ulimit -c unlimited
# 시스템 전역 설정 (Ubuntu/Debian)
# /etc/security/limits.conf 에 추가:
# * soft core unlimited
다음은 간단한 bash 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# systemd 서비스의 코어 덤프 저장 위치 확인
coredumpctl list
coredumpctl info
coredumpctl dump -o /tmp/core ./myapp
GDB 스크립트로 자동 분석
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# analyze_crash.gdb
set pagination off
run
bt full
info registers
info threads
quit
gdb -batch -x analyze_crash.gdb ./myapp
3. LLDB 완전 예제 (macOS)
환경: macOS, Clang
macOS에서는 GDB 대신 LLDB가 기본 디버거입니다. 명령어가 GDB와 유사하지만 일부 문법이 다릅니다.
LLDB 기본 명령어
다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# LLDB로 프로그램 실행
lldb ./myapp
# LLDB 내부 명령 (GDB와 대응)
(lldb) run # 실행
(lldb) b main # main에 브레이크포인트 (break의 약자)
(lldb) b file.cpp:42 # file.cpp 42번째 줄
(lldb) b func -c "x > 10" # 조건부 브레이크포인트
(lldb) c # continue
(lldb) n # next
(lldb) s # step
(lldb) finish # 현재 함수 반환까지
(lldb) p x # print x
(lldb) p *ptr # 포인터 역참조
(lldb) bt # backtrace
(lldb) bt all # 모든 스레드 백트레이스
(lldb) frame variable # 현재 프레임의 지역 변수
(lldb) f 2 # frame 2로 이동
(lldb) quit # 종료
macOS 코어 덤프 분석
아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# macOS에서 코어 덤프 생성 (크래시 시)
# 기본 위치: /cores/core.PID
# LLDB로 코어 분석
lldb -c /cores/core.12345 ./myapp
# 또는
lldb ./myapp
(lldb) target create -c /cores/core.12345 ./myapp
(lldb) bt all
LLDB 고급 예제: 멀티스레드 크래시
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// race_example.cpp
#include <thread>
#include <vector>
#include <atomic>
std::vector<int> shared_data;
std::atomic<bool> done{false};
void writer() {
for (int i = 0; i < 1000; ++i) {
shared_data.push_back(i); // 데이터 레이스 가능
}
done = true;
}
void reader() {
while (!done) {
for (size_t i = 0; i < shared_data.size(); ++i) {
(void)shared_data[i]; // 레이스
}
}
}
int main() {
std::thread t1(writer);
std::thread t2(reader);
t1.join();
t2.join();
return 0;
}
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# LLDB로 스레드별 백트레이스
lldb ./race_example
(lldb) run
# 크래시 시
(lldb) thread list
(lldb) thread select 1
(lldb) bt
(lldb) thread select 2
(lldb) bt
4. AddressSanitizer (ASan)
개념
AddressSanitizer는 메모리 오류를 런타임에 탐지하는 도구입니다. 버퍼 오버플로우, 언더플로우, use-after-free, double-free 등을 즉시 보고합니다.
아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
flowchart LR
subgraph normal[일반 빌드]
N1[버그 있는 코드] --> N2[나중에 크래시]
end
subgraph asan[ASan 빌드]
A1[버그 있는 코드] --> A2[즉시 탐지·보고]
end
ASan 활성화
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# GCC
g++ -std=c++17 -g -fsanitize=address -fno-omit-frame-pointer -o myapp main.cpp
# Clang
clang++ -std=c++17 -g -fsanitize=address -fno-omit-frame-pointer -o myapp main.cpp
주의: -O0가 아니어도 동작하지만, -fno-omit-frame-pointer가 있으면 스택 트레이스가 정확합니다.
ASan 완전 예제: 버퍼 오버플로우
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// asan_example.cpp
#include <cstring>
#include <iostream>
void vulnerable_copy(const char* src) {
char buf[16];
strcpy(buf, src); // 오버플로우: src가 16자보다 길면
std::cout << buf << "\n";
}
int main() {
vulnerable_copy("short");
vulnerable_copy("this is a very long string that overflows");
return 0;
}
clang++ -std=c++17 -g -fsanitize=address -fno-omit-frame-pointer -o asan_example asan_example.cpp
./asan_example
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# ASan 출력 예시
=================================================================
==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc...
#0 0x... in strcpy
#1 0x... in vulnerable_copy(...) at asan_example.cpp:6
#2 0x... in main at asan_example.cpp:12
...
=================================================================
ASan 완전 예제: use-after-free
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// uaf_example.cpp
#include <iostream>
int* get_ptr() {
int x = 42;
return &x; // 스택 주소 반환 (이미 잘못된 패턴)
}
void use_after_free() {
int* p = new int(100);
delete p;
*p = 200; // use-after-free
}
int main() {
use_after_free();
return 0;
}
clang++ -std=c++17 -g -fsanitize=address -fno-omit-frame-pointer -o uaf_example uaf_example.cpp
./uaf_example
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# ASan 출력 예시
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x...
#0 0x... in use_after_free() at uaf_example.cpp:10
#1 0x... in main at uaf_example.cpp:14
...
ASan 옵션
아래 코드는 bash를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 환경 변수로 동작 제어
export ASAN_OPTIONS=detect_leaks=1:abort_on_error=1:symbolize=1
# LeakSanitizer도 함께 (ASan에 포함됨)
# detect_leaks=1 이 기본
5. ThreadSanitizer (TSan)
개념
ThreadSanitizer는 데이터 레이스를 런타임에 탐지합니다. 두 스레드가 동시에 같은 메모리에 접근하고, 그 중 하나라도 쓰기이면 데이터 레이스입니다.
아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph race[데이터 레이스]
T1[스레드 1: 읽기/쓰기] --> M[공유 메모리]
T2[스레드 2: 읽기/쓰기] --> M
end
subgraph fix[해결]
F1[뮤텍스]
F2[atomic]
F3[스레드 로컬]
end
TSan 활성화
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# GCC (일부 버전)
g++ -std=c++17 -g -fsanitize=thread -o myapp main.cpp
# Clang (권장)
clang++ -std=c++17 -g -fsanitize=thread -o myapp main.cpp
주의: ASan과 TSan은 동시에 사용할 수 없습니다. 각각 별도 빌드로 테스트합니다.
TSan 완전 예제: 데이터 레이스
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// tsan_example.cpp
#include <thread>
#include <vector>
#include <chrono>
int counter = 0; // 공유 변수, 동기화 없음
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 데이터 레이스
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "counter = " << counter << "\n";
return 0;
}
clang++ -std=c++17 -g -fsanitize=thread -o tsan_example tsan_example.cpp
./tsan_example
아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# TSan 출력 예시
==================
WARNING: ThreadSanitizer: data race (pid=12345)
Write of size 4 at 0x... by thread T1:
#0 increment() at tsan_example.cpp:10
#1 ...
Previous read of size 4 at 0x... by thread T2:
#0 increment() at tsan_example.cpp:10
#1 ...
==================
TSan으로 수정된 예제
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// tsan_fixed.cpp
#include <thread>
#include <atomic>
#include <iostream>
std::atomic<int> counter{0};
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // atomic으로 레이스 제거
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "counter = " << counter.load() << "\n";
return 0;
}
6. 구조화 로깅
개념
프로덕션에서는 구조화 로깅이 필수입니다. 단순 printf 대신 레벨, 타임스탬프, 컨텍스트(파일, 라인, 함수), JSON 등으로 출력하면 로그 수집·검색·알림에 유리합니다.
spdlog 기반 로깅 예제
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// logging_example.cpp
#include <spdlog/spdlog.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <memory>
void setup_logging() {
// 콘솔 + 로테이팅 파일
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
"logs/app.log", 1024 * 1024 * 10, 3);
std::vector<spdlog::sink_ptr> sinks{console_sink, file_sink};
auto logger = std::make_shared<spdlog::logger>("main", sinks.begin(), sinks.end());
logger->set_level(spdlog::level::debug);
logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%s:%#] %v");
spdlog::set_default_logger(logger);
}
int main() {
setup_logging();
spdlog::info("Application started");
spdlog::debug("Debug message with value: {}", 42);
spdlog::warn("Warning: low memory");
spdlog::error("Error: connection failed");
return 0;
}
# 빌드 (vcpkg 또는 시스템 패키지)
# vcpkg install spdlog
g++ -std=c++17 -O2 -o logging_example logging_example.cpp -lspdlog
JSON 구조화 로깅
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// json_logging.cpp
#include <spdlog/spdlog.h>
#include <nlohmann/json.hpp>
void log_structured(const std::string& event, const std::string& user_id, int status) {
nlohmann::json j;
j[event] = event;
j[user_id] = user_id;
j[status] = status;
j[timestamp] = std::chrono::system_clock::now().time_since_epoch().count();
spdlog::info("{}", j.dump());
}
int main() {
log_structured("login", "user123", 200);
log_structured("request_failed", "user456", 500);
return 0;
}
매크로 기반 소스 위치 자동 포함
아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// log_macro.hpp
#define LOG_TRACE(...) spdlog::trace(__VA_ARGS__)
#define LOG_DEBUG(...) spdlog::debug(__VA_ARGS__)
#define LOG_INFO(...) spdlog::info(__VA_ARGS__)
#define LOG_WARN(...) spdlog::warn(__VA_ARGS__)
#define LOG_ERROR(...) spdlog::error("[{}:{}] ", __FILE__, __LINE__); spdlog::error(__VA_ARGS__)
#define LOG_CRITICAL(...) spdlog::critical("[{}:{}] ", __FILE__, __LINE__); spdlog::critical(__VA_ARGS__)
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 사용 예
#include "log_macro.hpp"
void process_request(int id) {
LOG_INFO("Processing request id={}", id);
if (id < 0) {
LOG_ERROR("Invalid id: {}", id);
return;
}
// ...
}
7. 자주 발생하는 에러와 해결법
에러 1: “No symbol table” / ”??” 만 보임
원인: 디버그 심볼이 없거나, 릴리즈 빌드만 배포된 상태에서 코어 덤프 분석.
해결:
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 빌드 시 -g 포함
g++ -std=c++17 -g -O2 -o myapp main.cpp
# 릴리즈 + 심볼 분리 시, .debug 파일을 같은 디렉터리에 두고
gdb ./myapp -c core
# GDB가 자동으로 myapp.debug 로드
에러 2: 코어 덤프가 생성되지 않음
원인: ulimit -c가 0이거나, systemd가 다른 위치에 저장.
해결:
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
ulimit -c unlimited
# systemd 사용 시
coredumpctl list
coredumpctl dump -o /tmp/core ./myapp
에러 3: ASan/TSan으로 빌드 시 링크 에러
원인: 일부 라이브러리가 sanitizer와 호환되지 않음 (예: 정적 링크된 오래된 라이브러리).
해결:
# 해당 라이브러리를 동적 링크로 전환
# 또는 ASan/TSan 없이 해당 부분만 별도 테스트
에러 4: TSan이 “실제 레이스가 아닌데” 보고함 (False Positive)
원인: 초기화 순서, 또는 의도된 lock-free 패턴이 TSan에 의해 레이스로 오탐될 수 있음.
해결:
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// TSan 무시가 필요한 극히 제한된 경우 (주의해서 사용)
#if defined(__SANITIZE_THREAD__)
extern "C" void __tsan_ignore_begin();
extern "C" void __tsan_ignore_end();
#define TSAN_IGNORE do { __tsan_ignore_begin(); } while(0)
#define TSAN_IGNORE_END do { __tsan_ignore_end(); } while(0)
#else
#define TSAN_IGNORE do {} while(0)
#define TSAN_IGNORE_END do {} while(0)
#endif
대부분은 실제 레이스이므로, 무시보다는 동기화 추가를 권장합니다.
에러 5: GDB/LLDB에서 STL 컨테이너 내용이 안 보임
원인: pretty printer가 로드되지 않음.
해결:
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# GDB: Python pretty printer (보통 기본 포함)
(gdb) info pretty-printer
# 수동 로드 (GCC)
(gdb) source /usr/share/gcc-11/python/libstdcxx/v6/printers.py
(gdb) info pretty-printer
# LLDB: LLVM pretty printer
# lldb는 기본적으로 libc++/libstdc++ 프린터 포함
(lldb) type summary add -s "${var[0]}" std::vector
에러 6: “AddressSanitizer: alloc-dealloc-mismatch”
원인: new로 할당하고 free로 해제, 또는 malloc으로 할당하고 delete로 해제.
해결:
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 예
int* p = new int(42);
free(p);
// ✅ 올바른 예
int* p = new int(42);
delete p;
에러 7: 프로덕션에서 로그가 너무 많아 디스크 full
원인: 로그 레벨이 debug로 설정되어 있거나, 로테이션 없이 단일 파일에 계속 기록.
해결:
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 프로덕션: info 이상만
logger->set_level(spdlog::level::info);
// 로테이팅 파일 (크기·개수 제한)
auto sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
"app.log", 10 * 1024 * 1024, 5);
8. 모범 사례
1. 디버그 빌드와 릴리즈 빌드 분리
다음은 간단한 bash 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# CMake 예시
cmake -DCMAKE_BUILD_TYPE=Debug ..
# 또는
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-g" ..
- 개발:
Debug또는RelWithDebInfo로 충분한 심볼 - 프로덕션:
Release+ 분리된.debug파일 보관
2. CI에서 Sanitizer 빌드 필수화
아래 코드는 yaml를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# GitHub Actions 예시
- name: Build with ASan
run: |
cmake -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" ..
cmake --build .
- name: Build with TSan
run: |
cmake -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_FLAGS="-fsanitize=thread" ..
cmake --build .
3. 로그 레벨 환경 변수로 제어
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
const char* level_str = std::getenv("LOG_LEVEL");
if (level_str) {
if (strcmp(level_str, "debug") == 0)
logger->set_level(spdlog::level::debug);
else if (strcmp(level_str, "info") == 0)
logger->set_level(spdlog::level::info);
// ...
}
4. assert와 로깅 병행
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
#define ASSERT_MSG(cond, msg) \
do { \
if (!(cond)) { \
spdlog::critical("Assertion failed: {} at {}:{}", (msg), __FILE__, __LINE__); \
std::abort(); \
} \
} while(0)
5. 크래시 시 로그 flush
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::atexit( {
spdlog::default_logger()->flush();
});
// 시그널 핸들러에서도
void signal_handler(int sig) {
spdlog::critical("Received signal {}", sig);
spdlog::default_logger()->flush();
std::_Exit(128 + sig);
}
9. 프로덕션 패턴
패턴 1: 자체 크래시 덤프 수집
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// crash_handler.cpp
#include <csignal>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <spdlog/spdlog.h>
void write_backtrace_to_file(const std::string& path) {
std::ofstream out(path);
// 실제로는 backtrace() 또는 예외 스택을 기록
out << "Crash backtrace placeholder\n";
out.close();
}
void crash_handler(int sig) {
spdlog::critical("Crash signal: {}", sig);
spdlog::default_logger()->flush();
std::string path = "crash_dump_" + std::to_string(getpid()) + ".txt";
write_backtrace_to_file(path);
std::signal(sig, SIG_DFL);
std::raise(sig);
}
void install_crash_handlers() {
std::signal(SIGSEGV, crash_handler);
std::signal(SIGABRT, crash_handler);
std::signal(SIGFPE, crash_handler);
}
패턴 2: Breakpad/Crashpad 통합
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# Crashpad: Google의 크로스 플랫폼 크래시 리포터
# - minidump 생성
# - 서버 전송
# - 심볼 서버와 연동해 스택 심볼화
# 빌드
git clone https://github.com/google/crashpad
cd crashpad && python build/gyp_crashpad.py
ninja -C out/Default
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.
// Crashpad 초기화 예시 (의사 코드)
#include <crashpad/client/crashpad_client.h>
crashpad::CrashpadClient client;
client.StartHandler(handler_path, db_path, url, annotations);
client.SetFirstExceptionHandler();
패턴 3: 구조화 로그 + ELK/Loki 수집
// JSON 로그로 stdout 출력 → Docker/ systemd가 수집 → ELK/Loki로 전송
logger->set_pattern("%v"); // JSON만 출력
logger->info(R"({"event":"request","path":"{}","status":{}})", path, status);
패턴 4: 디버그 빌드 전용 상세 로깅
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
#ifdef NDEBUG
#define LOG_VERBOSE(...) ((void)0)
#else
#define LOG_VERBOSE(...) spdlog::debug(__VA_ARGS__)
#endif
패턴 5: 메모리 프로파일링 (개발 전용)
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# Heob (Windows), Valgrind (Linux)로 메모리 누수 확인
valgrind --leak-check=full --show-leak-kinds=all ./myapp
# 또는 ASan + LSan
ASAN_OPTIONS=detect_leaks=1 ./myapp
10. 정리
도구별 요약
| 도구 | 용도 | 빌드 옵션 | 비고 |
|---|---|---|---|
| GDB | 크래시 분석, 단계 실행 | -g -O0 | Linux |
| LLDB | 크래시 분석, 단계 실행 | -g -O0 | macOS |
| ASan | 메모리 오류 | -fsanitize=address | 오버헤드 ~2x |
| TSan | 데이터 레이스 | -fsanitize=thread | ASan과 동시 사용 불가 |
| LSan | 메모리 누수 | ASan에 포함 | detect_leaks=1 |
| spdlog | 구조화 로깅 | -lspdlog | 프로덕션 추천 |
구현 체크리스트
- 디버그 빌드에
-g포함 - 프로덕션용 심볼 분리 보관
- 코어 덤프 활성화 (
ulimit -c unlimited) - CI에서 ASan/TSan 빌드 및 테스트
- 구조화 로깅 (레벨, 타임스탬프, 컨텍스트)
- 로그 로테이션 설정
- 크래시 핸들러 또는 Breakpad/Crashpad 통합
- 시그널 수신 시 로그 flush
참고 자료
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++ 디버깅: GDB/LLDB 완전 예제, AddressSanitizer·ThreadSanitizer, 구조화 로깅, 문제 시나리오, 흔한 에러, 모범 사례, 프로덕션 패턴까지 실전 코드로 다룹니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.