C++ 디버깅 완벽 가이드 | GDB·Sanitizer·메모리 누수·멀티스레드 디버깅 실전

C++ 디버깅 완벽 가이드 | GDB·Sanitizer·메모리 누수·멀티스레드 디버깅 실전

이 글의 핵심

C++ 디버깅 완벽 가이드에 대한 실전 가이드입니다. GDB·Sanitizer·메모리 누수·멀티스레드 디버깅 실전 등을 예제와 함께 상세히 설명합니다.

들어가며: “프로덕션에서 크래시가 나는데 재현이 안 돼요”

실무에서 겪는 디버깅 문제들

실제 C++ 개발 현장에서는 이런 문제를 겪습니다:

  • 프로덕션 크래시 — 개발 환경에서는 멀쩡한데 배포하면 랜덤 크래시
  • 메모리 누수 — 3일째 메모리 사용량이 계속 증가, 어디서 새는지 모름
  • 데이터 레이스 — 멀티스레드 환경에서 가끔 이상한 값이 나옴
  • 성능 저하 — 특정 함수에서 병목이 있는데 프로파일러 없이 찾기 어려움
  • 반복자 무효화 — 컨테이너 순회 중 크래시, 어디서 수정됐는지 추적 불가

이 글에서는 실전 시나리오를 기반으로 GDB 고급 기법, Sanitizer 활용, 멀티스레드 디버깅, 프로덕션 환경 디버깅까지 다룹니다.

목표:

  • GDB/LLDB 고급 기법 (watchpoint, conditional breakpoint, 코어 덤프 분석)
  • Sanitizer 완전 활용 (ASan, TSan, UBSan, MSan)
  • 메모리 누수 추적 (Valgrind, Heaptrack, 실전 패턴)
  • 멀티스레드 디버깅 (데이터 레이스, 데드락 탐지)
  • 프로덕션 디버깅 (로깅, 코어 덤프, 원격 디버깅)
  • 자주 하는 실수와 해결법
  • 프로덕션 패턴

요구 환경: C++17 이상, GDB 8.0+, Clang/GCC with Sanitizers


목차

  1. 문제 시나리오: 실무에서 겪는 디버깅 상황
  2. GDB/LLDB 고급 기법
  3. Sanitizer 완전 활용
  4. 메모리 누수 추적
  5. 멀티스레드 디버깅
  6. 프로덕션 환경 디버깅
  7. 완전한 디버깅 워크플로우
  8. 자주 발생하는 실수와 해결법
  9. 모범 사례·베스트 프랙티스
  10. 프로덕션 패턴
  11. 정리 및 체크리스트

1. 문제 시나리오: 실무에서 겪는 디버깅 상황

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

상황: 개발 환경에서는 정상 동작하는데, 프로덕션 배포 후 랜덤 크래시
증상: Segmentation fault, 코어 덤프 생성
원인: 릴리스 빌드 최적화로 숨겨진 버그 (초기화 안 된 변수, UB)
→ 코어 덤프 분석, UBSan으로 UB 탐지 필요

시나리오 2: “메모리가 계속 증가해요”

상황: 서버가 72시간 운영 후 메모리 8GB → 14GB로 증가
증상: 느린 메모리 누수, 재시작 전까지 회복 불가
원인: shared_ptr 순환 참조, 컨테이너에서 제거 안 된 객체
→ Valgrind, Heaptrack, ASan으로 누수 지점 추적

시나리오 3: “멀티스레드에서 가끔 이상한 값이 나와요”

상황: 10번 실행하면 1~2번 잘못된 결과 출력
증상: 데이터 레이스, 비결정적 동작
원인: 공유 변수 동기화 누락, 락 없는 접근
→ TSan으로 레이스 탐지, GDB로 스레드별 상태 확인

관련 글: 멀티스레드 기초에서 스레드 프로그래밍 기본 개념을 학습하세요.

시나리오 4: “컨테이너 순회 중 크래시해요”

상황: vector 순회 중 erase 호출 후 크래시
증상: iterator 무효화, Segmentation fault
원인: erase 후 반복자 갱신 안 함
→ GDB watchpoint로 컨테이너 수정 지점 추적

시나리오 5: “데드락이 발생해요”

상황: 멀티스레드 서버가 가끔 멈춤, CPU 사용률 0%
증상: 모든 스레드가 락 대기 상태
원인: 순환 락 대기 (A→B, B→A)
→ GDB로 스레드별 스택 확인, TSan으로 락 순서 분석

시나리오 6: “릴리스 빌드에서만 크래시해요”

상황: Debug 빌드는 정상, Release 빌드는 크래시
증상: 최적화로 인한 숨겨진 버그 노출
원인: 초기화 안 된 변수, 정의되지 않은 동작 (UB)
→ UBSan으로 UB 탐지, -O1로 중간 최적화 테스트

시나리오 7: “특정 입력에서만 크래시해요”

상황: 일반 입력은 정상, 특정 입력 (빈 문자열, 큰 숫자 등)에서만 크래시
증상: 경계 조건 (edge case) 처리 누락
원인: 입력 검증 부족, 배열 범위 체크 누락
→ Fuzzing (AFL, libFuzzer)으로 자동 테스트 케이스 생성
flowchart TB
    subgraph Problems["실무 디버깅 문제"]
        P1[프로덕션 크래시]
        P2[메모리 누수]
        P3[데이터 레이스]
        P4[반복자 무효화]
        P5[데드락]
    end
    subgraph Tools["디버깅 도구"]
        T1[GDB/LLDB + 코어 덤프]
        T2[Valgrind/ASan]
        T3[TSan]
        T4[Watchpoint]
        T5[스레드 분석]
    end
    P1 --> T1
    P2 --> T2
    P3 --> T3
    P4 --> T4
    P5 --> T5

2. GDB/LLDB 고급 기법

기본 사용법 복습

#include <iostream>
#include <vector>

int buggyFunction(int x) {
    int* ptr = nullptr;
    if (x > 10) {
        ptr = new int(x);
    }
    return *ptr;  // x <= 10이면 크래시
}

int main() {
    std::cout << buggyFunction(5) << std::endl;
    return 0;
}
# 컴파일 (디버그 심볼 포함)
g++ -g -O0 buggy.cpp -o buggy

# GDB 시작
gdb ./buggy

# 기본 명령어
(gdb) break buggyFunction    # 함수에 브레이크포인트
(gdb) run                    # 실행
(gdb) next                   # 다음 줄 (함수 넘어감)
(gdb) step                   # 다음 줄 (함수 안으로)
(gdb) print ptr              # 변수 출력
(gdb) backtrace              # 스택 트레이스
(gdb) continue               # 계속 실행

Watchpoint: 변수 변경 추적

문제: “이 변수가 언제 어디서 바뀌는지 모르겠어요”

#include <iostream>
#include <thread>
#include <vector>

int global_counter = 0;

void increment() {
    for (int i = 0; i < 1000; ++i) {
        ++global_counter;  // 어디서 바뀌는지 추적하고 싶음
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment);
    }
    for (auto& t : threads) {
        t.join();
    }
    std::cout << "Counter: " << global_counter << std::endl;
    return 0;
}

주의사항: 하드웨어 워치포인트 개수는 CPU마다 제한이 있어, 거대한 구조체 전체 감시는 느리거나 실패할 수 있습니다.

# GDB Watchpoint 사용
(gdb) break main
(gdb) run
(gdb) watch global_counter              # 변수 변경 시 중단
(gdb) continue
# global_counter가 변경될 때마다 멈춤
(gdb) backtrace                         # 어느 함수에서 변경했는지 확인

Conditional Breakpoint: 조건부 중단

문제: “반복문 1000번 중 특정 조건에서만 멈추고 싶어요”

#include <iostream>
#include <vector>

int main() {
    std::vector<int> data(1000);
    for (int i = 0; i < 1000; ++i) {
        data[i] = i * 2;
        // i가 500일 때만 확인하고 싶음
    }
    return 0;
}
# 조건부 브레이크포인트
(gdb) break 7 if i == 500               # i가 500일 때만 중단
(gdb) run

# 복잡한 조건
(gdb) break myFunction if ptr == nullptr && x > 100

# 조건 변경
(gdb) condition 1 i == 750              # 브레이크포인트 1번의 조건 변경

주의사항: 일부 최적화로 지역 변수가 레지스터만 쓰이면 조건식이 기대대로 동작하지 않을 수 있어 -O0를 권장합니다.

코어 덤프 분석

프로덕션에서 크래시 발생 시:

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

# 프로그램 실행 (크래시 시 core 파일 생성)
./myapp
# Segmentation fault (core dumped)

# 코어 덤프로 디버깅
gdb ./myapp core

# 크래시 지점 확인
(gdb) backtrace
(gdb) frame 0
(gdb) info locals              # 지역 변수 확인
(gdb) print this               # 객체 상태 확인

멀티스레드 디버깅

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

std::mutex mtx;
int shared_data = 0;

void worker(int id) {
    for (int i = 0; i < 100; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++shared_data;
        std::cout << "Thread " << id << ": " << shared_data << std::endl;
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(worker, i);
    }
    for (auto& t : threads) {
        t.join();
    }
    return 0;
}
# 멀티스레드 디버깅
(gdb) info threads                      # 모든 스레드 목록
(gdb) thread 2                          # 스레드 2로 전환
(gdb) backtrace                         # 해당 스레드의 스택
(gdb) thread apply all backtrace        # 모든 스레드의 스택 출력

# 스레드별 브레이크포인트
(gdb) break worker thread 3             # 스레드 3에서만 중단

주의사항: 논블로킹·파이버 환경에서는 스레드 번호가 흔들릴 수 있어 재현 스크립트와 함께 쓰는 것이 좋습니다.

GDB 스크립트 자동화

# debug.gdb 파일 생성
break main
run
print argc
print argv[0]
continue
# 스크립트 실행
gdb -x debug.gdb ./myapp

# 또는 GDB 내에서
(gdb) source debug.gdb

주의사항: 상대 경로는 GDB의 현재 작업 디렉터리 기준이라, 스크립트는 빌드 디렉터리에 두거나 절대 경로를 쓰세요.

Pretty Printing (STL 컨테이너)

#include <vector>
#include <map>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::map<std::string, int> m = {{"a", 1}, {"b", 2}};
    return 0;
}
# GDB에서 STL 예쁘게 출력
(gdb) print vec
# $1 = std::vector of length 5, capacity 5 = {1, 2, 3, 4, 5}

(gdb) print m
# $2 = std::map with 2 elements = {["a"] = 1, ["b"] = 2}

# Pretty printer 활성화 (없으면)
# ~/.gdbinit에 추가:
# python
# import sys
# sys.path.insert(0, '/usr/share/gcc/python')
# from libstdcxx.v6.printers import register_libstdcxx_printers
# register_libstdcxx_printers(None)
# end

LLDB (macOS 기본 디버거)

# LLDB 기본 사용법 (GDB와 유사)
lldb ./myapp

# 주요 명령어 비교
(lldb) breakpoint set --name main       # GDB: break main
(lldb) run                              # GDB: run
(lldb) next                             # GDB: next
(lldb) step                             # GDB: step
(lldb) print variable                   # GDB: print variable
(lldb) bt                               # GDB: backtrace
(lldb) continue                         # GDB: continue

# Watchpoint
(lldb) watchpoint set variable global_counter
(lldb) watchpoint list

3. Sanitizer 완전 활용

AddressSanitizer (ASan): 메모리 오류 탐지

탐지 가능한 버그:

  • Use-after-free
  • Heap buffer overflow
  • Stack buffer overflow
  • Use-after-return
  • Memory leaks
#include <iostream>

int main() {
    int* arr = new int[10];
    delete[] arr;
    
    // Use-after-free
    std::cout << arr[0] << std::endl;  // 💥 ASan이 탐지
    
    return 0;
}
# ASan 활성화 컴파일
g++ -fsanitize=address -g -O1 use_after_free.cpp -o test
# 또는 Clang
clang++ -fsanitize=address -g -O1 use_after_free.cpp -o test

# 실행
./test

# 출력 예시:
# =================================================================
# ==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x...
# READ of size 4 at 0x... thread T0
#     #0 0x... in main use_after_free.cpp:7
# ...
# freed by thread T0 here:
#     #0 0x... in operator delete
#     #1 0x... in main use_after_free.cpp:5

Heap Buffer Overflow 탐지:

#include <iostream>

int main() {
    int* arr = new int[10];
    
    // Buffer overflow
    arr[10] = 42;  // 💥 ASan이 탐지 (인덱스 범위 초과)
    
    delete[] arr;
    return 0;
}

Stack Buffer Overflow 탐지:

#include <cstring>

int main() {
    char buffer[10];
    strcpy(buffer, "This is too long!");  // 💥 ASan이 탐지
    return 0;
}

ThreadSanitizer (TSan): 데이터 레이스 탐지

#include <iostream>
#include <thread>

int global_counter = 0;  // 보호되지 않은 공유 변수

void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++global_counter;  // 💥 TSan이 데이터 레이스 탐지
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << global_counter << std::endl;
    return 0;
}
# TSan 활성화 컴파일
g++ -fsanitize=thread -g -O1 data_race.cpp -o test -pthread

# 실행
./test

# 출력 예시:
# ==================
# WARNING: ThreadSanitizer: data race (pid=12345)
#   Write of size 4 at 0x... by thread T2:
#     #0 increment() data_race.cpp:7
#   Previous write of size 4 at 0x... by thread T1:
#     #0 increment() data_race.cpp:7

수정 버전 (뮤텍스 사용):

#include <iostream>
#include <thread>
#include <mutex>

int global_counter = 0;
std::mutex mtx;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++global_counter;  // ✅ 이제 안전
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << global_counter << std::endl;
    return 0;
}

UndefinedBehaviorSanitizer (UBSan): 정의되지 않은 동작 탐지

#include <iostream>
#include <limits>

int main() {
    // 정수 오버플로우
    int max = std::numeric_limits<int>::max();
    int overflow = max + 1;  // 💥 UBSan이 탐지
    
    // 0으로 나누기
    int x = 10;
    int y = 0;
    int result = x / y;  // 💥 UBSan이 탐지
    
    // 널 포인터 역참조
    int* ptr = nullptr;
    int value = *ptr;  // 💥 UBSan이 탐지
    
    // 잘못된 캐스팅
    class Base { virtual ~Base() {} };
    class Derived : public Base {};
    Base* b = new Base();
    Derived* d = static_cast<Derived*>(b);  // 💥 UBSan이 탐지
    
    return 0;
}
# UBSan 활성화 컴파일
g++ -fsanitize=undefined -g -O1 ub.cpp -o test

# 실행
./test

# 출력 예시:
# ub.cpp:7:20: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
# ub.cpp:11:19: runtime error: division by zero

MemorySanitizer (MSan): 초기화 안 된 메모리 탐지

#include <iostream>

int main() {
    int x;  // 초기화 안 됨
    
    if (x > 10) {  // 💥 MSan이 탐지
        std::cout << "x is large" << std::endl;
    }
    
    int* arr = new int[10];  // 초기화 안 됨
    std::cout << arr[0] << std::endl;  // 💥 MSan이 탐지
    
    delete[] arr;
    return 0;
}
# MSan 활성화 (Clang만 지원)
clang++ -fsanitize=memory -g -O1 uninit.cpp -o test

# 실행
./test

Sanitizer 조합 사용

# ASan + UBSan 조합 (권장)
g++ -fsanitize=address,undefined -g -O1 program.cpp -o test

# 프로덕션 빌드에는 사용하지 말 것 (성능 오버헤드 큼)
# 개발/테스트 환경에서만 사용

Sanitizer 옵션 설정

# ASan 옵션
export ASAN_OPTIONS=detect_leaks=1:check_initialization_order=1

# TSan 옵션
export TSAN_OPTIONS=second_deadlock_stack=1

# 실행
./test

4. 메모리 누수 추적

Valgrind: 메모리 프로파일링

#include <iostream>

void leakyFunction() {
    int* leak = new int[100];
    // delete[] leak;  // 누락! 💥
}

int main() {
    for (int i = 0; i < 10; ++i) {
        leakyFunction();
    }
    return 0;
}
# Valgrind로 메모리 누수 탐지
g++ -g leak.cpp -o leak
valgrind --leak-check=full --show-leak-kinds=all ./leak

# 출력 예시:
# ==12345== HEAP SUMMARY:
# ==12345==     in use at exit: 4,000 bytes in 10 blocks
# ==12345==   total heap usage: 10 allocs, 0 frees, 4,000 bytes allocated
# ==12345==
# ==12345== 4,000 bytes in 10 blocks are definitely lost in loss record 1 of 1
# ==12345==    at 0x...: operator new
# ==12345==    by 0x...: leakyFunction() (leak.cpp:4)
# ==12345==    by 0x...: main (leak.cpp:9)

shared_ptr 순환 참조 탐지

#include <iostream>
#include <memory>

class Node {
public:
    std::shared_ptr<Node> next;
    ~Node() {
        std::cout << "Node destroyed" << std::endl;
    }
};

int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    
    // 순환 참조 생성 💥
    node1->next = node2;
    node2->next = node1;
    
    // main 종료 시 소멸자가 호출되지 않음 (메모리 누수)
    return 0;
}

해결: weak_ptr 사용:

#include <iostream>
#include <memory>

class Node {
public:
    std::weak_ptr<Node> next;  // ✅ weak_ptr로 변경
    ~Node() {
        std::cout << "Node destroyed" << std::endl;
    }
};

int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    
    node1->next = node2;
    node2->next = node1;
    
    // ✅ 이제 정상적으로 소멸됨
    return 0;
}

ASan으로 메모리 누수 탐지

# ASan은 기본적으로 메모리 누수도 탐지
g++ -fsanitize=address -g leak.cpp -o leak
./leak

# 출력:
# =================================================================
# ==12345==ERROR: LeakSanitizer: detected memory leaks
#
# Direct leak of 400 byte(s) in 1 object(s) allocated from:
#     #0 0x... in operator new
#     #1 0x... in leakyFunction() leak.cpp:4
#     #2 0x... in main leak.cpp:9

Heaptrack: 힙 메모리 프로파일링

# Heaptrack 설치 (Linux)
sudo apt install heaptrack

# 프로그램 실행
heaptrack ./myapp

# 결과 분석
heaptrack_gui heaptrack.myapp.12345.gz

5. 멀티스레드 디버깅

데이터 레이스 실전 시나리오

#include <iostream>
#include <thread>
#include <vector>

class BankAccount {
private:
    int balance = 1000;
    
public:
    void withdraw(int amount) {
        // 💥 데이터 레이스: balance 읽기/쓰기가 원자적이지 않음
        if (balance >= amount) {
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
            balance -= amount;
        }
    }
    
    int getBalance() const { return balance; }
};

int main() {
    BankAccount account;
    std::vector<std::thread> threads;
    
    // 10개 스레드가 동시에 100원씩 인출
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back([&account]() {
            for (int j = 0; j < 10; ++j) {
                account.withdraw(100);
            }
        });
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    // 예상: 1000 - (10 * 10 * 100) = -9000 또는 음수
    // 실제: 매번 다른 값 (데이터 레이스)
    std::cout << "Final balance: " << account.getBalance() << std::endl;
    
    return 0;
}

TSan으로 탐지:

g++ -fsanitize=thread -g bank.cpp -o bank -pthread
./bank

# WARNING: ThreadSanitizer: data race

해결: 뮤텍스 사용:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

class BankAccount {
private:
    int balance = 1000;
    std::mutex mtx;  // ✅ 뮤텍스 추가
    
public:
    void withdraw(int amount) {
        std::lock_guard<std::mutex> lock(mtx);  // ✅ 락 획득
        if (balance >= amount) {
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
            balance -= amount;
        }
    }
    
    int getBalance() {
        std::lock_guard<std::mutex> lock(mtx);
        return balance;
    }
};

데드락 탐지

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mutex1, mutex2;

void thread1() {
    std::lock_guard<std::mutex> lock1(mutex1);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::lock_guard<std::mutex> lock2(mutex2);  // 💥 데드락
    std::cout << "Thread 1" << std::endl;
}

void thread2() {
    std::lock_guard<std::mutex> lock2(mutex2);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::lock_guard<std::mutex> lock1(mutex1);  // 💥 데드락
    std::cout << "Thread 2" << std::endl;
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);
    
    t1.join();
    t2.join();
    
    return 0;
}

GDB로 데드락 분석:

# 프로그램이 멈추면 Ctrl+C로 중단
g++ -g -pthread deadlock.cpp -o deadlock
./deadlock
# (멈춤)
# 다른 터미널에서:
gdb -p $(pidof deadlock)

(gdb) info threads
(gdb) thread apply all backtrace

# 각 스레드가 어떤 락을 기다리는지 확인

해결: std::scoped_lock (C++17):

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mutex1, mutex2;

void thread1() {
    std::scoped_lock lock(mutex1, mutex2);  // ✅ 데드락 방지
    std::cout << "Thread 1" << std::endl;
}

void thread2() {
    std::scoped_lock lock(mutex1, mutex2);  // ✅ 항상 같은 순서로 락 획득
    std::cout << "Thread 2" << std::endl;
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);
    
    t1.join();
    t2.join();
    
    return 0;
}

반복자 무효화 디버깅

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // 💥 반복자 무효화
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        if (*it % 2 == 0) {
            vec.erase(it);  // erase 후 it가 무효화됨
        }
    }
    
    return 0;
}

GDB Watchpoint로 추적:

(gdb) break main
(gdb) run
(gdb) watch vec._M_impl._M_start  # vector 내부 포인터 감시
(gdb) continue
# erase 호출 시 중단됨

해결: erase-remove idiom:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // ✅ erase-remove idiom
    vec.erase(
        std::remove_if(vec.begin(), vec.end(),
             { return x % 2 == 0; }),
        vec.end()
    );
    
    // 또는 C++20 erase_if
    // std::erase_if(vec,  { return x % 2 == 0; });
    
    return 0;
}

6. 프로덕션 환경 디버깅

구조화된 로깅

#include <iostream>
#include <fstream>
#include <chrono>
#include <iomanip>
#include <sstream>
#include <mutex>

class Logger {
public:
    enum Level { DEBUG, INFO, WARNING, ERROR, FATAL };
    
private:
    static std::mutex mtx_;
    static std::ofstream file_;
    static Level min_level_;
    
public:
    static void init(const std::string& filename, Level min_level = INFO) {
        file_.open(filename, std::ios::app);
        min_level_ = min_level;
    }
    
    static void log(Level level, const std::string& message,
                    const char* file = __builtin_FILE(),
                    int line = __builtin_LINE(),
                    const char* func = __builtin_FUNCTION()) {
        if (level < min_level_) return;
        
        std::lock_guard<std::mutex> lock(mtx_);
        
        auto now = std::chrono::system_clock::now();
        auto time = std::chrono::system_clock::to_time_t(now);
        auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
            now.time_since_epoch()) % 1000;
        
        std::ostringstream oss;
        oss << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S")
            << "." << std::setfill('0') << std::setw(3) << ms.count()
            << " [" << levelToString(level) << "] "
            << "[" << file << ":" << line << "] "
            << "[" << func << "] "
            << message << std::endl;
        
        std::string log_line = oss.str();
        std::cout << log_line;
        if (file_.is_open()) {
            file_ << log_line;
            file_.flush();
        }
    }
    
private:
    static const char* levelToString(Level level) {
        switch (level) {
            case DEBUG:   return "DEBUG";
            case INFO:    return "INFO";
            case WARNING: return "WARN";
            case ERROR:   return "ERROR";
            case FATAL:   return "FATAL";
            default:      return "UNKNOWN";
        }
    }
};

std::mutex Logger::mtx_;
std::ofstream Logger::file_;
Logger::Level Logger::min_level_ = Logger::INFO;

// 매크로로 편리하게 사용
#define LOG_DEBUG(msg) Logger::log(Logger::DEBUG, msg, __FILE__, __LINE__, __func__)
#define LOG_INFO(msg) Logger::log(Logger::INFO, msg, __FILE__, __LINE__, __func__)
#define LOG_ERROR(msg) Logger::log(Logger::ERROR, msg, __FILE__, __LINE__, __func__)

int main() {
    Logger::init("app.log", Logger::DEBUG);
    
    LOG_INFO("프로그램 시작");
    LOG_DEBUG("디버그 정보");
    LOG_ERROR("에러 발생");
    
    return 0;
}

코어 덤프 자동 수집

#!/bin/bash
# core_dump_setup.sh

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

# 코어 덤프 파일 위치 설정
echo "/var/crash/core.%e.%p.%t" | sudo tee /proc/sys/kernel/core_pattern

# 프로그램 실행
./myapp

# 크래시 발생 시 /var/crash/에 코어 덤프 생성

원격 디버깅 (gdbserver)

# 서버 (프로덕션 환경)
gdbserver :1234 ./myapp

# 클라이언트 (개발 환경)
gdb ./myapp
(gdb) target remote server_ip:1234
(gdb) continue

프로덕션 크래시 리포트

#include <csignal>
#include <cstdlib>
#include <iostream>
#include <execinfo.h>
#include <unistd.h>

void signalHandler(int sig) {
    std::cerr << "Error: signal " << sig << std::endl;
    
    // 스택 트레이스 출력
    void* array[10];
    size_t size = backtrace(array, 10);
    
    std::cerr << "Stack trace:" << std::endl;
    backtrace_symbols_fd(array, size, STDERR_FILENO);
    
    exit(1);
}

int main() {
    // 시그널 핸들러 등록
    signal(SIGSEGV, signalHandler);
    signal(SIGABRT, signalHandler);
    
    // 프로그램 로직
    int* ptr = nullptr;
    *ptr = 42;  // 크래시 발생 시 스택 트레이스 출력
    
    return 0;
}

7. 완전한 디버깅 워크플로우

단계별 디버깅 프로세스

flowchart TB
    Start[버그 발견] --> Reproduce[재현 가능한가?]
    Reproduce -->|Yes| Minimal[최소 재현 코드 작성]
    Reproduce -->|No| Logging[로깅 추가]
    Logging --> Reproduce
    
    Minimal --> Hypothesis[가설 수립]
    Hypothesis --> Tool{도구 선택}
    
    Tool -->|메모리 오류| ASan[AddressSanitizer]
    Tool -->|데이터 레이스| TSan[ThreadSanitizer]
    Tool -->|정의되지 않은 동작| UBSan[UBSan]
    Tool -->|일반 디버깅| GDB[GDB/LLDB]
    
    ASan --> Verify[검증]
    TSan --> Verify
    UBSan --> Verify
    GDB --> Verify
    
    Verify -->|해결| Test[테스트 작성]
    Verify -->|미해결| Hypothesis
    
    Test --> Done[완료]

실전 예제: 복합 버그 디버깅

#include <iostream>
#include <thread>
#include <vector>
#include <memory>

class Resource {
public:
    int* data;
    
    Resource() : data(new int[100]) {
        std::cout << "Resource created" << std::endl;
    }
    
    ~Resource() {
        delete[] data;
        std::cout << "Resource destroyed" << std::endl;
    }
};

std::shared_ptr<Resource> global_resource;

void worker() {
    // 💥 여러 버그가 섞여 있음
    for (int i = 0; i < 1000; ++i) {
        if (!global_resource) {
            global_resource = std::make_shared<Resource>();
        }
        
        // 데이터 레이스
        global_resource->data[i % 100] = i;
        
        if (i == 500) {
            global_resource.reset();  // 다른 스레드가 사용 중일 수 있음
        }
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(worker);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

디버깅 단계:

# 1단계: TSan으로 데이터 레이스 탐지
g++ -fsanitize=thread -g bug.cpp -o bug -pthread
./bug
# WARNING: ThreadSanitizer: data race

# 2단계: ASan으로 메모리 오류 탐지
g++ -fsanitize=address -g bug.cpp -o bug -pthread
./bug
# ERROR: AddressSanitizer: heap-use-after-free

# 3단계: GDB로 상세 분석
g++ -g bug.cpp -o bug -pthread
gdb ./bug
(gdb) break worker
(gdb) run
(gdb) info threads
(gdb) thread apply all backtrace

수정 버전:

#include <iostream>
#include <thread>
#include <vector>
#include <memory>
#include <mutex>

class Resource {
public:
    int* data;
    
    Resource() : data(new int[100]) {
        std::cout << "Resource created" << std::endl;
    }
    
    ~Resource() {
        delete[] data;
        std::cout << "Resource destroyed" << std::endl;
    }
};

std::shared_ptr<Resource> global_resource;
std::mutex resource_mutex;

void worker() {
    for (int i = 0; i < 1000; ++i) {
        std::shared_ptr<Resource> local_resource;
        
        {
            std::lock_guard<std::mutex> lock(resource_mutex);
            if (!global_resource) {
                global_resource = std::make_shared<Resource>();
            }
            local_resource = global_resource;  // ✅ 로컬 복사본 유지
        }
        
        // ✅ 이제 안전하게 접근 가능
        local_resource->data[i % 100] = i;
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(worker);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

8. 자주 발생하는 실수와 해결법

실수 1: 디버그 심볼 없이 컴파일

# ❌ 잘못된 방법
g++ -O2 program.cpp -o program

# ✅ 올바른 방법
g++ -g -O0 program.cpp -o program  # 디버그 빌드
g++ -g -O2 program.cpp -o program  # 릴리스 빌드 (디버그 심볼 포함)

실수 2: 최적화로 인한 변수 최적화

int main() {
    int x = 10;
    int y = x + 5;  // 컴파일러가 y = 15로 최적화
    return y;
}
# GDB에서 x를 출력하려 하면 "optimized out" 메시지
(gdb) print x
# $1 = <optimized out>

# 해결: -O0으로 컴파일
g++ -g -O0 program.cpp -o program

실수 3: Sanitizer와 최적화 레벨

# ❌ -O0은 일부 버그를 숨길 수 있음
g++ -fsanitize=address -g -O0 program.cpp

# ✅ -O1 또는 -O2 권장 (Sanitizer 공식 권장사항)
g++ -fsanitize=address -g -O1 program.cpp

실수 4: 멀티스레드 프로그램에서 -pthread 누락

# ❌ 링크 에러 또는 런타임 오류
g++ -fsanitize=thread -g program.cpp

# ✅ -pthread 추가
g++ -fsanitize=thread -g program.cpp -pthread

실수 5: 코어 덤프 크기 제한

# 코어 덤프가 생성되지 않으면 확인
ulimit -c
# 0이면 비활성화 상태

# 무제한으로 설정
ulimit -c unlimited

# 영구 설정 (/etc/security/limits.conf에 추가)
# * soft core unlimited
# * hard core unlimited

실수 6: Valgrind와 ASan 동시 사용

# ❌ 충돌 발생
g++ -fsanitize=address program.cpp -o program
valgrind ./program

# ✅ 하나만 사용
# ASan 사용 시
g++ -fsanitize=address program.cpp -o program
./program

# Valgrind 사용 시
g++ -g program.cpp -o program
valgrind --leak-check=full ./program

9. 모범 사례·베스트 프랙티스

개발 환경 설정

# CMakeLists.txt에 디버그 옵션 추가
if(CMAKE_BUILD_TYPE MATCHES Debug)
    add_compile_options(-g -O0)
    add_compile_options(-fsanitize=address,undefined)
    add_link_options(-fsanitize=address,undefined)
endif()

if(CMAKE_BUILD_TYPE MATCHES RelWithDebInfo)
    add_compile_options(-g -O2)
endif()

CI/CD에 Sanitizer 통합

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Build with ASan
        run: |
          mkdir build
          cd build
          cmake -DCMAKE_BUILD_TYPE=Debug ..
          make
      
      - name: Run tests
        run: |
          cd build
          export ASAN_OPTIONS=detect_leaks=1
          ctest --output-on-failure

어설션 전략

#include <cassert>
#include <iostream>

// 디버그 빌드에서만 활성화되는 어설션
#ifdef NDEBUG
    #define DEBUG_ASSERT(condition, message) ((void)0)
#else
    #define DEBUG_ASSERT(condition, message) \
        if (!(condition)) { \
            std::cerr << "Assertion failed: " << message << std::endl; \
            std::cerr << "File: " << __FILE__ << ", Line: " << __LINE__ << std::endl; \
            std::abort(); \
        }
#endif

// 릴리스 빌드에서도 활성화되는 어설션
#define RELEASE_ASSERT(condition, message) \
    if (!(condition)) { \
        std::cerr << "Fatal error: " << message << std::endl; \
        std::cerr << "File: " << __FILE__ << ", Line: " << __LINE__ << std::endl; \
        std::abort(); \
    }

int main() {
    int x = 10;
    
    // 개발 중에만 체크
    DEBUG_ASSERT(x > 0, "x must be positive");
    
    // 항상 체크 (중요한 불변 조건)
    RELEASE_ASSERT(x < 100, "x must be less than 100");
    
    return 0;
}

로깅 레벨 전략

// 개발: DEBUG 레벨
Logger::init("app.log", Logger::DEBUG);

// 스테이징: INFO 레벨
Logger::init("app.log", Logger::INFO);

// 프로덕션: WARNING 레벨
Logger::init("app.log", Logger::WARNING);

테스트 주도 디버깅

#include <cassert>
#include <iostream>

// 버그 재현 테스트 작성
void test_divide_by_zero() {
    try {
        int result = divide(10, 0);
        assert(false && "Should throw exception");
    } catch (const std::invalid_argument& e) {
        std::cout << "Test passed: " << e.what() << std::endl;
    }
}

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

10. 프로덕션 패턴

패턴 1: 헬스 체크 엔드포인트

#include <iostream>
#include <chrono>
#include <thread>
#include <atomic>

class HealthMonitor {
private:
    std::atomic<bool> is_healthy_{true};
    std::chrono::steady_clock::time_point last_heartbeat_;
    
public:
    void heartbeat() {
        last_heartbeat_ = std::chrono::steady_clock::now();
    }
    
    bool isHealthy() {
        auto now = std::chrono::steady_clock::now();
        auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
            now - last_heartbeat_).count();
        
        // 10초 이상 heartbeat 없으면 비정상
        return elapsed < 10;
    }
    
    void setUnhealthy() {
        is_healthy_ = false;
    }
};

// HTTP 서버에서 /health 엔드포인트 제공
// GET /health -> {"status": "ok", "uptime": 12345}

패턴 2: 메트릭 수집

#include <iostream>
#include <atomic>
#include <chrono>

class Metrics {
private:
    std::atomic<uint64_t> request_count_{0};
    std::atomic<uint64_t> error_count_{0};
    std::atomic<uint64_t> total_latency_ms_{0};
    
public:
    void recordRequest(uint64_t latency_ms, bool is_error = false) {
        ++request_count_;
        total_latency_ms_ += latency_ms;
        if (is_error) {
            ++error_count_;
        }
    }
    
    void report() {
        uint64_t requests = request_count_.load();
        uint64_t errors = error_count_.load();
        uint64_t latency = total_latency_ms_.load();
        
        std::cout << "Requests: " << requests << std::endl;
        std::cout << "Errors: " << errors << std::endl;
        if (requests > 0) {
            std::cout << "Avg latency: " << (latency / requests) << "ms" << std::endl;
            std::cout << "Error rate: " << (errors * 100.0 / requests) << "%" << std::endl;
        }
    }
};

패턴 3: 그레이스풀 셧다운

#include <csignal>
#include <atomic>
#include <iostream>
#include <thread>

std::atomic<bool> shutdown_requested{false};

void signalHandler(int signal) {
    if (signal == SIGINT || signal == SIGTERM) {
        std::cout << "Shutdown requested..." << std::endl;
        shutdown_requested = true;
    }
}

int main() {
    signal(SIGINT, signalHandler);
    signal(SIGTERM, signalHandler);
    
    std::cout << "Server started. Press Ctrl+C to stop." << std::endl;
    
    while (!shutdown_requested) {
        // 메인 루프
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    
    std::cout << "Shutting down gracefully..." << std::endl;
    // 리소스 정리
    
    std::cout << "Shutdown complete." << std::endl;
    return 0;
}

패턴 4: 순환 버퍼 로깅

#include <array>
#include <string>
#include <mutex>

template<size_t N>
class CircularLogBuffer {
private:
    std::array<std::string, N> buffer_;
    size_t index_ = 0;
    std::mutex mtx_;
    
public:
    void log(const std::string& message) {
        std::lock_guard<std::mutex> lock(mtx_);
        buffer_[index_] = message;
        index_ = (index_ + 1) % N;
    }
    
    void dump() {
        std::lock_guard<std::mutex> lock(mtx_);
        std::cout << "=== Last " << N << " log entries ===" << std::endl;
        for (size_t i = 0; i < N; ++i) {
            size_t idx = (index_ + i) % N;
            if (!buffer_[idx].empty()) {
                std::cout << buffer_[idx] << std::endl;
            }
        }
    }
};

// 크래시 시 최근 로그만 덤프
CircularLogBuffer<100> crash_log;

11. 정리 및 체크리스트

디버깅 도구 선택 가이드

문제 유형추천 도구사용 시기
메모리 누수ASan, Valgrind개발/테스트
데이터 레이스TSan멀티스레드 개발
정의되지 않은 동작UBSan모든 빌드
일반 크래시GDB/LLDB개발 중
프로덕션 크래시코어 덤프 + GDB프로덕션
성능 병목perf, gprof최적화 단계
초기화 안 된 메모리MSanClang 환경

Sanitizer 성능 비교

도구속도 오버헤드메모리 오버헤드탐지 범위컴파일러 지원
ASan2x2~3x메모리 오류, 누수GCC, Clang, MSVC
TSan5~15x5~10x데이터 레이스GCC, Clang
UBSan1.2x최소정의되지 않은 동작GCC, Clang, MSVC
MSan3x2x초기화 안 된 메모리Clang만
Valgrind10~50x최소메모리 전반모든 바이너리

권장 조합: ASan + UBSan (일상 개발), TSan (멀티스레드), Valgrind (정밀 분석)

개발 환경 체크리스트

# ✅ 디버그 빌드 설정
- [ ] -g 플래그 추가
- [ ] -O0 또는 -O1 사용
- [ ] Sanitizer 활성화 (-fsanitize=address,undefined)
- [ ] 컴파일 경고 최대화 (-Wall -Wextra -Wpedantic)

# ✅ 테스트 환경
- [ ] 단위 테스트 작성
- [ ] CI/CD에 Sanitizer 통합
- [ ] 코드 커버리지 측정
- [ ] Fuzzing 테스트 (AFL, libFuzzer)

# ✅ 프로덕션 준비
- [ ] 로깅 시스템 구축 (레벨별, 파일 로테이션)
- [ ] 코어 덤프 활성화 (ulimit -c unlimited)
- [ ] 헬스 체크 엔드포인트
- [ ] 메트릭 수집 (요청 수, 에러율, 레이턴시)
- [ ] 그레이스풀 셧다운 (SIGTERM 핸들러)
- [ ] 모니터링 알림 (Prometheus, Grafana)

실전 팁: 디버깅 시간 단축

  1. 항상 Sanitizer와 함께 개발

    # .bashrc 또는 .zshrc에 추가
    alias g++debug='g++ -g -O1 -fsanitize=address,undefined -Wall -Wextra'
    
    # 사용
    g++debug myapp.cpp -o myapp
  2. GDB 설정 파일 (.gdbinit)

    # ~/.gdbinit
    set print pretty on
    set print array on
    set print array-indexes on
    set pagination off
    
    # 자주 쓰는 명령어 단축
    define pv
        print $arg0
    end
  3. 빠른 재현 스크립트

    #!/bin/bash
    # reproduce_bug.sh
    
    # 디버그 빌드
    g++ -g -O0 -fsanitize=address bug.cpp -o bug
    
    # 여러 번 실행 (재현 확인)
    for i in {1..10}; do
        echo "Run $i"
        ./bug || break
    done
  4. 로그 레벨 동적 변경

    // 환경 변수로 로그 레벨 제어
    const char* log_level = std::getenv("LOG_LEVEL");
    if (log_level && std::string(log_level) == "DEBUG") {
        Logger::setLevel(Logger::DEBUG);
    }
    
    // 실행 시
    // LOG_LEVEL=DEBUG ./myapp

디버깅 워크플로우 요약

  1. 재현: 버그를 재현 가능한 최소 코드로 만들기
  2. 가설: 원인 가설 수립
  3. 도구: 적절한 디버깅 도구 선택
  4. 분석: GDB, Sanitizer로 원인 분석
  5. 수정: 버그 수정
  6. 검증: 테스트 작성 및 실행
  7. 문서화: 버그 원인과 해결법 기록

빠른 참조: 디버깅 체크리스트

# 🔍 버그 발견 시 즉시 확인할 것
 재현 가능한가? (재현 불가 로깅 추가)
 최소 재현 코드 작성 완료?
 컴파일 경고 모두 확인? (gcc -Wall -Wextra)
 Sanitizer 활성화? (ASan + UBSan)

# 🛠️ 도구 선택
 메모리 오류 ASan 또는 Valgrind
 데이터 레이스 TSan
 크래시 GDB + 코어 덤프
 성능 병목 perf, gprof

# ✅ 해결 후
 테스트 케이스 작성
 코드 리뷰 요청
 문서화 (버그 원인과 해결법)

트러블슈팅: 빠른 문제 해결

증상원인해결법
Segfaultnullptr 역참조, 배열 범위 초과ASan 활성화, GDB로 스택 확인
메모리 누수delete 누락, 순환 참조Valgrind 또는 ASan leak 탐지
데이터 레이스동기화 누락TSan 활성화, 뮤텍스 추가
데드락순환 락 대기GDB로 스레드 스택 확인, scoped_lock 사용
랜덤 크래시UB, 초기화 안 된 변수UBSan, MSan 활성화
느린 빌드PCH 미사용, 단일 스레드ccache, Ninja, 병렬 빌드

다음 단계

  • GDB 고급 가이드에서 더 깊이 있는 디버깅 기법 학습
  • Sanitizer 완전 가이드에서 각 Sanitizer의 고급 기능 활용
  • 메모리 누수 디버깅에서 복잡한 메모리 문제 해결법 학습
  • 멀티스레드 기초에서 스레드 프로그래밍 기초 학습
  • Segfault 디버깅에서 크래시 원인 분석 심화

FAQ

Q1: 디버깅을 어디서부터 시작해야 하나요?

A:

  1. 버그를 재현 가능한 최소 코드로 만들기
  2. ASan + UBSan으로 컴파일해서 실행 (대부분의 버그 자동 탐지)
  3. 여전히 문제가 있으면 GDB로 단계별 실행

Q2: GDB vs LLDB 어떤 걸 써야 하나요?

A:

  • Linux: GDB (표준)
  • macOS: LLDB (기본 제공, Xcode 통합)
  • Windows: Visual Studio Debugger 또는 GDB (MinGW)
  • 명령어는 거의 유사하므로 하나만 익히면 됩니다.

Q3: 프로덕션에서 Sanitizer를 사용해도 되나요?

A:

  • ❌ 권장하지 않음: 성능 오버헤드가 크고 (2~5배), 메모리 사용량 증가
  • 개발/테스트 환경에서만 사용
  • 프로덕션에서는 로깅 + 코어 덤프 + 모니터링 사용

Q4: Valgrind vs ASan 어떤 게 더 좋나요?

A:

  • ASan: 빠름 (2~3배 느림), 컴파일 타임에 통합, 더 많은 버그 탐지
  • Valgrind: 느림 (10~50배 느림), 별도 실행, 정밀한 메모리 추적
  • 권장: 일상적으로는 ASan, 정밀 분석이 필요하면 Valgrind

Q5: 멀티스레드 버그를 어떻게 재현하나요?

A:

  • TSan으로 컴파일 (대부분의 레이스 자동 탐지)
  • std::this_thread::sleep_for로 타이밍 조절
  • 스레드 수를 늘려서 경합 증가
  • stress 도구로 부하 생성

Q6: 디버깅 학습 리소스는?

A:

  • : “The Art of Debugging with GDB, DDD, and Eclipse”
  • 문서: GDB 공식 문서, Sanitizer 문서
  • 실습: 의도적으로 버그를 만들고 디버깅 연습
  • 오픈소스: 유명 프로젝트의 이슈 트래커에서 버그 수정 과정 학습

Q7: 프로덕션 환경에서 디버깅할 때 주의사항은?

A:

  • 성능 영향 최소화: 로깅 레벨을 적절히 설정 (WARNING 이상만)
  • 민감 정보 보호: 비밀번호, API 키 등을 로그에 남기지 않기
  • 코어 덤프 크기 제한: ulimit -c 설정으로 디스크 공간 관리
  • 원격 디버깅 보안: gdbserver 사용 시 방화벽 설정
  • 재현 환경 구축: 프로덕션과 동일한 환경에서 테스트

Q8: ASan을 켰는데 메모리 사용량이 너무 많아요

A: ASan은 메모리 사용량을 2~3배 증가시킵니다. 이는 정상입니다.

  • 개발/테스트 환경에서만 사용하세요
  • 메모리가 부족하면 작은 테스트 케이스로 분할
  • 또는 Valgrind 사용 (느리지만 메모리 사용량 적음)
  • CI/CD에서는 메모리 충분한 인스턴스 사용

Q9: GDB에서 “optimized out” 메시지가 나와요

A: 컴파일러 최적화로 변수가 제거되었습니다.

# 해결법: 최적화 비활성화
g++ -g -O0 program.cpp  # -O0 사용

# 또는 특정 함수만 최적화 비활성화
__attribute__((optimize("O0")))
void debugFunction() {
    // 함수는 최적화되지 않음
}

Q10: 데드락을 어떻게 예방하나요?

A:

  • 락 순서 일관성: 항상 같은 순서로 락 획득
  • std::scoped_lock 사용 (C++17, 데드락 방지)
  • 타임아웃 설정: try_lock_for() 사용
  • 락 홀딩 시간 최소화: 락 안에서 긴 작업 피하기
  • TSan으로 검증: 개발 중 항상 TSan 활성화

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

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

  • C++ GDB/LLDB | cout 100개 찍어도 못 찾은 버그, 디버거로 5분 만에 해결
  • C++ 런타임 검증: AddressSanitizer와 ThreadSanitizer 완벽 가이드 [#41-2]
  • C++ Segmentation fault | core dump
  • C++ Memory Leak | “메모리 누수” 가이드
  • C++ std::thread 입문 | join 누락·디태치 남용 등 자주 하는 실수 3가지와 해결법
  • C++ Sanitizers | “새니타이저” 가이드

관련 글

  • C++ 디버깅 실전 가이드 | gdb, LLDB, Visual Studio 완벽 활용
  • C++ Segmentation fault 원인 5가지와 디버깅 방법 | GDB로 추적하기
  • C++ 메모리 누수 찾기 | Valgrind·ASan으로
  • C++ GDB |
  • C++ Memory Leak |