C++ Sanitizers | "새니타이저" 가이드

C++ Sanitizers | "새니타이저" 가이드

이 글의 핵심

C++ Sanitizers에 대한 실전 가이드입니다.

들어가며

Sanitizers는 런타임에 메모리 오류, 데이터 레이스, 정의되지 않은 동작을 탐지하는 강력한 도구입니다. Valgrind보다 빠르고, 정확한 스택 추적을 제공하여 버그를 빠르게 찾을 수 있습니다.


1. Sanitizers 종류

주요 Sanitizers

# AddressSanitizer (ASan) - 메모리 오류
g++ -fsanitize=address -g program.cpp -o program

# ThreadSanitizer (TSan) - 데이터 레이스
g++ -fsanitize=thread -g program.cpp -o program

# UndefinedBehaviorSanitizer (UBSan) - 정의되지 않은 동작
g++ -fsanitize=undefined -g program.cpp -o program

# MemorySanitizer (MSan) - 초기화되지 않은 메모리
g++ -fsanitize=memory -g program.cpp -o program

# LeakSanitizer (LSan) - 메모리 누수 (ASan 포함)
g++ -fsanitize=leak -g program.cpp -o program

Sanitizer 비교

Sanitizer탐지 대상성능 오버헤드호환성
ASan메모리 오류2xASan+UBSan
TSan데이터 레이스5-15x단독
UBSan정의되지 않은 동작1.2xASan+UBSan
MSan초기화 안된 메모리3x단독
LSan메모리 누수낮음ASan 포함

2. AddressSanitizer (ASan)

버퍼 오버플로우

// bug1.cpp
#include <iostream>

int main() {
    int arr[10];
    
    // 버그: 범위 초과
    for (int i = 0; i <= 10; ++i) {
        arr[i] = i;
    }
    
    return 0;
}

컴파일 및 실행:

$ g++ -fsanitize=address -g bug1.cpp -o bug1
$ ./bug1

=================================================================
==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc...
WRITE of size 4 at 0x7ffc... thread T0
    #0 0x... in main bug1.cpp:8
    #1 0x... in __libc_start_main
    
Address 0x7ffc... is located in stack of thread T0 at offset 40 in frame
    #0 0x... in main bug1.cpp:4

Use-After-Free

// bug2.cpp
#include <iostream>

int main() {
    int* ptr = new int(42);
    delete ptr;
    
    // 버그: 해제 후 사용
    std::cout << *ptr << std::endl;
    
    return 0;
}

ASan 출력:

$ g++ -fsanitize=address -g bug2.cpp -o bug2
$ ./bug2

=================================================================
==12346==ERROR: AddressSanitizer: heap-use-after-free on address 0x602...
READ of size 4 at 0x602... thread T0
    #0 0x... in main bug2.cpp:7
    
0x602... is located 0 bytes inside of 4-byte region [0x602..., 0x602...)
freed by thread T0 here:
    #0 0x... in operator delete(void*)
    #1 0x... in main bug2.cpp:5

메모리 누수

// bug3.cpp
#include <iostream>

int main() {
    // 버그: 메모리 누수
    int* leak = new int(42);
    
    return 0;  // delete 누락
}

ASan 출력:

$ g++ -fsanitize=address -g bug3.cpp -o bug3
$ ./bug3

=================================================================
==12347==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 4 byte(s) in 1 object(s) allocated from:
    #0 0x... in operator new(unsigned long)
    #1 0x... in main bug3.cpp:5

3. ThreadSanitizer (TSan)

데이터 레이스

// race.cpp
#include <thread>
#include <iostream>

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 << std::endl;
    
    return 0;
}

TSan 출력:

$ g++ -fsanitize=thread -g race.cpp -o race
$ ./race

==================
WARNING: ThreadSanitizer: data race (pid=12348)
  Write of size 4 at 0x... by thread T1:
    #0 increment() race.cpp:7
  
  Previous write of size 4 at 0x... by thread T2:
    #0 increment() race.cpp:7

수정:

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

std::atomic<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 << std::endl;  // 200000
    
    return 0;
}

4. UndefinedBehaviorSanitizer (UBSan)

정의되지 않은 동작

// ub.cpp
#include <iostream>

int main() {
    // 버그 1: 0으로 나누기
    int x = 0;
    int y = 5 / x;
    
    // 버그 2: 부호있는 정수 오버플로우
    int max = 2147483647;
    int overflow = max + 1;
    
    // 버그 3: nullptr 역참조
    int* ptr = nullptr;
    int value = *ptr;
    
    // 버그 4: 배열 범위 초과
    int arr[10];
    int index = 15;
    int val = arr[index];
    
    return 0;
}

UBSan 출력:

$ g++ -fsanitize=undefined -g ub.cpp -o ub
$ ./ub

ub.cpp:6:15: runtime error: division by zero
ub.cpp:10:23: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
ub.cpp:14:17: runtime error: load of null pointer of type 'int'
ub.cpp:19:17: runtime error: index 15 out of bounds for type 'int [10]'

5. 실전 예제: CI/CD 통합

CMake 설정

# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)

# Sanitizer 옵션
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
option(ENABLE_UBSAN "Enable UBSanitizer" OFF)

if(ENABLE_ASAN)
    add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
    add_link_options(-fsanitize=address)
endif()

if(ENABLE_TSAN)
    add_compile_options(-fsanitize=thread)
    add_link_options(-fsanitize=thread)
endif()

if(ENABLE_UBSAN)
    add_compile_options(-fsanitize=undefined)
    add_link_options(-fsanitize=undefined)
endif()

add_executable(myapp main.cpp)

빌드:

# ASan 빌드
cmake -DENABLE_ASAN=ON ..
make

# TSan 빌드
cmake -DENABLE_TSAN=ON ..
make

# UBSan 빌드
cmake -DENABLE_UBSAN=ON ..
make

GitHub Actions

# .github/workflows/sanitizers.yml
name: Sanitizers

on: [push, pull_request]

jobs:
  asan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build with ASan
        run: |
          g++ -fsanitize=address -g src/*.cpp -o test_asan
      
      - name: Run Tests
        run: ./test_asan
  
  tsan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build with TSan
        run: |
          g++ -fsanitize=thread -g src/*.cpp -o test_tsan
      
      - name: Run Tests
        run: ./test_tsan
  
  ubsan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build with UBSan
        run: |
          g++ -fsanitize=undefined -g src/*.cpp -o test_ubsan
      
      - name: Run Tests
        run: ./test_ubsan

6. 자주 발생하는 문제

문제 1: 성능 오버헤드

// Sanitizer 성능 영향:
// - ASan: 2배 느림
// - TSan: 5-15배 느림
// - UBSan: 1.2배 느림

// ✅ 개발/테스트에만 사용
// ✅ 릴리스 빌드에서 제거

문제 2: 호환성

# ❌ ASan과 TSan 동시 사용 불가
g++ -fsanitize=address,thread program.cpp  # 에러

# ✅ 개별 빌드
g++ -fsanitize=address program.cpp  # ASan 빌드
g++ -fsanitize=thread program.cpp   # TSan 빌드

# ✅ ASan + UBSan 조합 가능
g++ -fsanitize=address,undefined program.cpp

문제 3: 거짓 양성

// 의도적 패턴이 탐지될 수 있음

// 억제 방법 1: 속성
__attribute__((no_sanitize("address")))
void intentionalOverflow() {
    // ...
}

// 억제 방법 2: 억제 파일
// asan.supp
// leak:intentionalLeak
# 억제 파일 사용
ASAN_OPTIONS=suppressions=asan.supp ./program

문제 4: 디버그 정보 필수

# ❌ 디버그 정보 없음 (스택 추적 불완전)
g++ -fsanitize=address program.cpp

# ✅ -g 플래그 추가
g++ -fsanitize=address -g program.cpp

# ✅ 최적화 낮춤 (더 정확한 추적)
g++ -fsanitize=address -g -O1 program.cpp

7. 실전 예제: 버그 찾기

// buggy_program.cpp
#include <iostream>
#include <vector>
#include <thread>

class DataProcessor {
    std::vector<int> data;
    int counter = 0;
    
public:
    // 버그 1: 메모리 누수
    void leakyFunction() {
        int* leak = new int(42);
        // delete 누락
    }
    
    // 버그 2: 버퍼 오버플로우
    void bufferOverflow() {
        int arr[10];
        for (int i = 0; i <= 10; ++i) {
            arr[i] = i;
        }
    }
    
    // 버그 3: Use-after-free
    void useAfterFree() {
        int* ptr = new int(42);
        delete ptr;
        std::cout << *ptr << std::endl;
    }
    
    // 버그 4: 데이터 레이스
    void dataRace() {
        auto increment = [this]() {
            for (int i = 0; i < 100000; ++i) {
                ++counter;  // 동기화 없음
            }
        };
        
        std::thread t1(increment);
        std::thread t2(increment);
        
        t1.join();
        t2.join();
    }
    
    // 버그 5: 정수 오버플로우
    void integerOverflow() {
        int max = 2147483647;
        int overflow = max + 1;
        std::cout << overflow << std::endl;
    }
};

int main() {
    DataProcessor processor;
    
    // 각 버그를 개별적으로 테스트
    // processor.leakyFunction();
    // processor.bufferOverflow();
    // processor.useAfterFree();
    // processor.dataRace();
    // processor.integerOverflow();
    
    return 0;
}

테스트 스크립트:

#!/bin/bash

echo "=== AddressSanitizer ==="
g++ -fsanitize=address -g buggy_program.cpp -o test_asan
./test_asan

echo "=== ThreadSanitizer ==="
g++ -fsanitize=thread -g buggy_program.cpp -o test_tsan
./test_tsan

echo "=== UBSanitizer ==="
g++ -fsanitize=undefined -g buggy_program.cpp -o test_ubsan
./test_ubsan

8. Sanitizer 옵션

환경 변수

# ASan 옵션
export ASAN_OPTIONS=detect_leaks=1:halt_on_error=0

# TSan 옵션
export TSAN_OPTIONS=history_size=7:halt_on_error=0

# UBSan 옵션
export UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=0

# 프로그램 실행
./program

유용한 ASan 옵션

# 메모리 누수 탐지
ASAN_OPTIONS=detect_leaks=1

# 스택 사용량 확인
ASAN_OPTIONS=detect_stack_use_after_return=1

# 컨테이너 오버플로우
ASAN_OPTIONS=detect_container_overflow=1

# 로그 파일
ASAN_OPTIONS=log_path=asan.log

# 조합
ASAN_OPTIONS=detect_leaks=1:log_path=asan.log:halt_on_error=0 ./program

정리

핵심 요약

  1. ASan: 메모리 오류 (버퍼, use-after-free, 누수)
  2. TSan: 데이터 레이스 (멀티스레딩)
  3. UBSan: 정의되지 않은 동작 (오버플로우, 0 나누기)
  4. MSan: 초기화 안된 메모리
  5. LSan: 메모리 누수 (ASan 포함)
  6. 성능: 개발/테스트 전용 (2-15배 느림)

Sanitizer 선택 가이드

버그 유형Sanitizer사용 시기
버퍼 오버플로우ASan항상
메모리 누수ASan (LSan)항상
Use-after-freeASan항상
데이터 레이스TSan멀티스레딩
정수 오버플로우UBSan산술 연산
초기화 안된 메모리MSan복잡한 초기화

실전 팁

개발 워크플로우:

  • 로컬 개발: ASan + UBSan 빌드로 테스트
  • 멀티스레딩: TSan 빌드로 별도 테스트
  • CI/CD: 모든 Sanitizer로 자동 테스트
  • 릴리스: Sanitizer 제거

성능:

  • ASan이 가장 유용 (2배 오버헤드)
  • TSan은 멀티스레딩 코드에만
  • UBSan은 오버헤드 낮음 (항상 사용 고려)

디버깅:

  • -g 플래그로 스택 추적 개선
  • -O1 최적화로 정확도 향상
  • 환경 변수로 세부 옵션 조정

다음 단계

  • C++ Valgrind
  • C++ GDB
  • C++ Memory Leak

관련 글

  • C++ Valgrind |
  • C++ GDB |
  • C++ Heap Corruption |
  • C++ Memory Leak |
  • C++ Use After Free |