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 | 메모리 오류 | 2x | ASan+UBSan |
| TSan | 데이터 레이스 | 5-15x | 단독 |
| UBSan | 정의되지 않은 동작 | 1.2x | ASan+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
정리
핵심 요약
- ASan: 메모리 오류 (버퍼, use-after-free, 누수)
- TSan: 데이터 레이스 (멀티스레딩)
- UBSan: 정의되지 않은 동작 (오버플로우, 0 나누기)
- MSan: 초기화 안된 메모리
- LSan: 메모리 누수 (ASan 포함)
- 성능: 개발/테스트 전용 (2-15배 느림)
Sanitizer 선택 가이드
| 버그 유형 | Sanitizer | 사용 시기 |
|---|---|---|
| 버퍼 오버플로우 | ASan | 항상 |
| 메모리 누수 | ASan (LSan) | 항상 |
| Use-after-free | ASan | 항상 |
| 데이터 레이스 | 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 |