ASan과 UBSan 실전 설정 — 컴파일 옵션과 런타임 동작
#황금 조합 다시 보기
Ch 1에서 본 황금 조합을 다시 적습니다.
-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1각 옵션이 왜 그 자리에 있는지를 정확히 짚으면, 설정 디버깅이 빨라집니다. 이번 장은 이 한 줄을 실제 프로젝트에서 운영하는 디테일을 모읍니다.
#컴파일과 링크 둘 다 적용해야 한다
가장 흔한 첫 실수입니다. Sanitizer는 컴파일 시점에 코드 계측을 하고, 링크 시점에 sanitizer 런타임 라이브러리를 함께 묶습니다. 둘 중 하나라도 빠지면 빌드는 되지만 동작이 이상해집니다.
# 잘못된 예 — 컴파일에만gcc -c -fsanitize=address main.c -o main.ogcc main.o -o myapp # 링크에 없음 → undefined symbol
# 올바른 예 — 둘 다gcc -c -fsanitize=address main.c -o main.ogcc -fsanitize=address main.o -o myapp # 링크에도 명시이 때문에 빌드 시스템 통합 시 컴파일러·링커 양쪽에 같은 sanitizer 옵션을 줍니다.
CFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1LDFLAGS += -fsanitize=address,undefinedCMake에서는 더 간단합니다 (target_compile_options + target_link_options).
target_compile_options(myapp PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer -O1 -g)target_link_options(myapp PRIVATE -fsanitize=address,undefined)#컴파일러 옵션 자세히
#-fno-omit-frame-pointer — 스택 트레이스 정확도
Sanitizer가 에러를 보고할 때, 어디서 발생했는지를 알려면 스택 트레이스가 필요합니다. 컴파일러는 최적화 차원에서 frame pointer를 생략하는데(-fomit-frame-pointer, 대부분 -O1 이상에서 기본 켜짐), 그러면 일부 함수가 트레이스에서 사라집니다.
-fno-omit-frame-pointer는 이걸 강제로 끄고 완전한 트레이스를 보장합니다.
ERROR: AddressSanitizer: heap-buffer-overflow on address 0x... #0 main.c:42 in process_data #1 main.c:67 in run_pipeline #2 main.c:103 in main이 줄들이 모두 보여야 디버깅이 가능합니다. 빠뜨리면 트레이스가 짧아지거나 ??로 채워집니다.
#-O1 — 약한 최적화
이 옵션은 조심해서 골라야 합니다.
| 최적화 | 효과 |
|---|---|
-O0 | 너무 약함. 컴파일러가 일부 UB를 그대로 흘려보내 sanitizer 오탐 증가. |
-O1 | 권장. 대부분의 UB 검사가 잘 동작하면서 속도도 견딜 만함. |
-O2/-O3 | UB 위반 코드를 제거해 sanitizer가 찾지 못할 수 있음. |
엄밀하게 말하면 sanitizer는 어느 최적화 레벨에서도 대부분 동작하지만, 어떤 UB는 컴파일러가 미리 제거해 ASan이 놓치는 경우가 있습니다. 그래서 sanitizer 빌드는 보통 -O1을 씁니다.
#-g — 디버그 심볼
Sanitizer 보고서가 파일명·줄 번호를 보여 주려면 -g가 필요합니다. 없으면 주소만 찍힙니다.
# -g 없이#0 0x40123f in process_data#1 0x401aef in run_pipeline
# -g 있게#0 in process_data /path/to/main.c:42#1 in run_pipeline /path/to/main.c:67-g는 모든 sanitizer 빌드에 필수입니다.
#환경 변수 — 런타임 동작 제어
Sanitizer는 환경 변수로 런타임 동작을 바꿉니다. 각 sanitizer가 자기 변수를 갖습니다.
ASAN_OPTIONS=...UBSAN_OPTIONS=...TSAN_OPTIONS=...LSAN_OPTIONS=...MSAN_OPTIONS=...값은 key=value:key=value:... 형식의 콜론 구분입니다.
#자주 쓰는 ASAN_OPTIONS
ASAN_OPTIONS=\detect_leaks=1:\halt_on_error=1:\abort_on_error=1:\print_stacktrace=1:\symbolize=1:\print_summary=1| 키 | 의미 |
|---|---|
detect_leaks=1 | LSan을 ASan에서 활성화 (Linux 기본 1, macOS 기본 0) |
halt_on_error=1 | 첫 에러에서 즉시 종료 (기본 0 — 계속 진행) |
abort_on_error=1 | exit()가 아니라 abort()로 종료 → coredump 생성 |
symbolize=1 | 주소를 함수명·줄 번호로 변환 (llvm-symbolizer 또는 addr2line 사용) |
print_stacktrace=1 | 모든 보고에 스택 트레이스 포함 |
print_summary=1 | 종료 시 요약 출력 |
strict_string_checks=1 | strcpy 등의 교차 영역 검사 (느려짐) |
check_initialization_order=1 | 전역 변수 초기화 순서 의존 검사 |
CI에서는 보통 halt_on_error=1:abort_on_error=1을 켜서 첫 실패가 프로세스 종료 코드 비-0으로 즉시 보고되도록 합니다.
#자주 쓰는 UBSAN_OPTIONS
UBSAN_OPTIONS=\halt_on_error=1:\print_stacktrace=1:\symbolize=1UBSan은 ASan보다 옵션이 적습니다. 가장 중요한 두 가지:
halt_on_error=1— UB를 만났을 때 즉시 종료.print_stacktrace=1— 어디서 UB가 났는지 트레이스 출력.
기본값은 경고만 출력하고 계속 진행입니다. CI에서는 거의 항상 halt_on_error=1로 켭니다.
#symbolize가 동작 안 할 때
ASan은 외부 심볼라이저를 호출해 주소→줄 번호 변환을 합니다. 환경에 llvm-symbolizer가 없으면 숫자 주소만 보입니다.
# llvm-symbolizer 경로 명시export ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer
# 또는 환경에 없으면 GCC 빌드용 addr2lineexport ASAN_SYMBOLIZER_PATH=$(which addr2line)Ubuntu/Debian: sudo apt install llvm로 설치하면 함께 들어옵니다. macOS: Xcode Command Line Tools에 포함.
#Suppression — 오탐 / 외부 라이브러리 무시
Sanitizer가 외부 라이브러리(OpenSSL, glibc, Qt)에서 우리가 못 고치는 자리를 보고할 때, suppression 파일로 그 자리만 무시할 수 있습니다.
# suppression 파일 — asan.suppleak:libcrypto.soleak:libssl.sointerceptor_via_fun:OpenSSL_*LSAN_OPTIONS=suppressions=asan.supp ./myappASAN_OPTIONS=suppressions=asan.supp ./myappUBSAN_OPTIONS=suppressions=ubsan.supp ./myapp각 sanitizer가 자기 suppression 파일을 따로 가집니다. 패턴은 함수명 / 모듈명 / 정규식을 지원합니다.
suppression 패턴 예:
| 패턴 | 의미 |
|---|---|
leak:my_known_leak_function | 함수명 |
leak:libfoo.so | 라이브러리 |
interceptor_via_fun:^OpenSSL_* | 정규식 (interceptor) |
signed-integer-overflow:somefile.cpp | UBSan 전용 |
Suppression은 최후의 수단입니다. 우리 코드의 버그를 가리면 안 됩니다. 외부 라이브러리·CRT의 알려진 false positive에만 씁니다.
#흔한 오탐과 함정
#1. 정적 초기화 순서 의존 (SIOF)
struct A { A() { /* uses B */ } };A a;
// b.cppstruct B { B() { /* init */ } };B b;전역 객체 a와 b의 초기화 순서가 .cpp 파일 사이에서 정의되지 않습니다. a의 생성자가 b를 쓰는데 b가 아직 초기화 안 됐을 수 있습니다.
ASan은 check_initialization_order=1로 이 자리를 잡지만, 기본은 꺼져 있습니다. 대신 Construct On First Use 관용으로 회피합니다.
B& get_b() { static B instance; // 첫 호출 시 초기화 — 순서 보장 return instance;}#2. STL 컨테이너 overflow 검사
std::vector<int> v(10);int x = v[20]; // ❌ container-overflow (ASan 옵션 필요)기본 ASan은 std::vector의 논리적 크기까지만 검사하고, 물리 capacity가 더 크면 그 안은 잡지 못합니다. 켜려면:
ASAN_OPTIONS=detect_container_overflow=1# 컴파일 시-D_LIBCPP_HAS_ASAN_CONTAINER_ANNOTATIONS # libc++libstdc++(GCC 기본)는 이미 활성화되어 있고, libc++(Clang/macOS)는 명시적으로 켜야 합니다.
#3. dlsym이 ASan과 충돌
// Sanitizer에 잘못 잡히는 패턴void* sym = dlsym(handle, "func");typedef int (*FN)(int);FN fn = (FN)sym;fn(42); // ASan이 false positive 보고 가능dlsym은 주소를 동적으로 얻어와서 호출하는데, sanitizer 계측 정보가 없습니다. 보통 외부 plugin 시스템에서 등장하고, 이 자리는 suppression으로 우회합니다.
#4. setjmp/longjmp와 sanitizer
longjmp로 스택을 풀어 버리는 코드는 ASan이 use-after-return false positive를 보고할 수 있습니다. C++ 예외와도 비슷한 문제 — sanitizer가 일부 스택 시나리오를 추적 못 합니다.
#5. 시그널 핸들러 안에서
void handler(int sig) { // ❌ malloc, printf 등 비-async-safe 호출 char* buf = malloc(100);}ASan은 시그널 핸들러 안에서의 할당을 의심할 수 있습니다. 표준 POSIX에서도 시그널 핸들러는 async-signal-safe 함수만 호출해야 합니다. ASan은 이를 상기시켜 주는 효과가 있습니다.
#어디까지 켜야 하나 — 결정 가이드
Sanitizer를 모든 빌드에 항상 켤 수는 없습니다. 오버헤드도 있고 일부 환경에서 동작 안 합니다. 다음 가이드로 결정합니다.
#켤 자리
- 로컬 개발 빌드: 기본 켜기.
ENABLE_SANITIZERS=ON을 디폴트. - 단위 테스트: 항상 켜기. 작은 단위 테스트는 sanitizer 오버헤드가 큰 문제 안 됨.
- CI의 PR 빌드: 켜기. PR마다 ASan+UBSan 빌드를 한 번 더 돌림.
- 통합 테스트의 일부: 큰 데이터셋은 sanitizer 빌드가 너무 느림 → 작은 시나리오만.
#끌 자리
- 릴리스 빌드: 무조건 끄기. 사용자에게 2배 느린 바이너리를 줄 수는 없습니다.
- 벤치마크: 끄기. 측정 결과가 의미 없어짐.
- 임베디드/모바일: 메모리 오버헤드 큼. 디바이스 위에서 못 돌릴 수 있음.
CMake에서는 옵션으로 분기합니다.
option(ENABLE_SANITIZERS "Enable ASan + UBSan" OFF)
if(ENABLE_SANITIZERS) add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) add_link_options(-fsanitize=address,undefined)endif()자세한 통합은 Ch 5에서 다룹니다.
#보고 읽기 — 실제 ASan 출력 해부
#include <string.h>int main() { char buf[8]; strcpy(buf, "Hello, World!"); return 0;}$ gcc -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 main.c -o test$ ./test===================================================================12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffe8c5f0a08 at pc 0x0000004012a3 bp 0x7ffe8c5f0930 sp 0x7ffe8c5f00d8WRITE of size 14 at 0x7ffe8c5f0a08 thread T0 #0 0x4012a2 in __interceptor_strcpy /usr/include/.../strcpy_interceptor.h:45 #1 0x401361 in main /path/to/main.c:4 #2 0x7f7c4a02d082 in __libc_start_main #3 0x40118d in _start
Address 0x7ffe8c5f0a08 is located in stack of thread T0 at offset 40 in frame #0 0x4012ce in main /path/to/main.c:2
This frame has 1 object(s): [32, 40) 'buf' (line 3) <== Memory access at offset 40 overflows this variableHINT: this may be a false positive if your program uses some custom stack unwind mechanismSUMMARY: AddressSanitizer: stack-buffer-overflow on main.c:4 in main==12345==ABORTING이 보고서가 말하는 것을 분해하면:
- 에러 종류:
stack-buffer-overflow— 스택 변수의 경계 침범 - 접근 종류:
WRITE of size 14— 14바이트를 쓰려 함 - 스택 트레이스:
main.c:4에서 strcpy 호출 — 우리 코드의 정확한 위치 - 할당 정보:
[32, 40)—buf라는 변수가 차지하는 영역 (8바이트) - 위반 위치:
offset 40— 변수 끝(40) 자리에 접근 → 1바이트만 넘어도 보고 - 요약 한 줄: 빠른 인지용
SUMMARY 한 줄이 CI 로그에서 가장 먼저 보이는 부분입니다. 이걸로 어디서 어떤 종류의 버그인지 즉시 파악합니다.
#정리
- ASan + UBSan은 컴파일과 링크 둘 다에 적용. 한쪽이라도 빠지면 미동작.
- 권장 옵션:
-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1. - 환경 변수
ASAN_OPTIONS/UBSAN_OPTIONS로 런타임 동작 제어. CI는halt_on_error=1권장. symbolize=1로 줄 번호 보고.ASAN_SYMBOLIZER_PATH로 심볼라이저 경로 명시 가능.- Suppression은 외부 라이브러리·알려진 false positive에만. 우리 코드 버그를 가리지 마라.
- 오탐 함정: SIOF, container overflow, dlsym, longjmp, async-signal-safe.
- 켤 자리: 로컬 개발, 단위 테스트, PR CI. 끌 자리: 릴리스, 벤치마크.
#다음 장 예고
Ch 3: LSan과 누수 분석에서는 메모리 누수에 집중합니다. LSan 보고서 해석, 알려진 누수 suppression, 일회성·반복적 분석 패턴.
#참고 자료
관련 글
Sanitizer 종류 비교 — ASan·UBSan·LSan·TSan·MSan
C/C++ 런타임 검사 도구 Sanitizer 계열의 역할, 종류별 선택, 실무 도입 순서.
LSan 누수 분석 — Stop-the-world Leak Detection 메커니즘
LeakSanitizer로 메모리 누수 추적 — 보고서 해석, suppression, 일회성·반복 분석 패턴.
TSan으로 데이터 레이스 디버깅 — Happens-before 추적
ThreadSanitizer로 멀티스레드 버그 추적 — happens-before 모델, false positive, atomic·mutex 통합.
이 글을 참조하는 글 (11)
- 포스트모템 자동화 — debuginfod·Minidump 파이프라인— Postmortem Debugging
- GDB로 Core 분석 — backtrace·info threads·py 활용— Postmortem Debugging
- Core Dump 생성 메커니즘 — kernel의 dump path 분석— Postmortem Debugging
- 운영 메모리 누수 진단 — long-running 프로세스의 진단 전략— Memory Diagnostics
- glibc 메모리 도구 — mtrace·mcheck·MALLOC_CHECK_— Memory Diagnostics
- jemalloc·tcmalloc Profiling — 운영 allocator의 진단 기능— Memory Diagnostics
- Sanitizer 종류 비교 — ASan·UBSan·LSan·TSan·MSan— Sanitizers
- Kernel Panic·Oops 메시지 해석 — Decoder Ring 만들기— Kernel Debugging
- GDB·LLDB 실전 팁 — STL·최적화 코드·시간 역행 디버깅— GDB and LLDB
- Core Dump 분석 기법 — gcore·coredumpctl·디버거 활용— GDB and LLDB
- GDB vs LLDB 분석 — 두 디버거의 설치·차이·선택 기준— GDB and LLDB