C++ 미정의 동작(UB) 완전 가이드
미정의 동작(Undefined Behavior, UB)은 C++ 표준이 동작을 규정하지 않은 코드 영역입니다. 컴파일러는 UB가 발생하지 않는다고 가정하고 공격적으로 최적화하므로, UB가 있는 코드는 디버그 빌드에서 정상 동작해도 릴리스 빌드에서 예상치 못한 결과를 낳습니다.
1. 부호 있는 정수 오버플로우
섹션 제목: “1. 부호 있는 정수 오버플로우”// 부호 있는 정수 오버플로우 → UBint x = INT_MAX;int y = x + 1; // UB! 컴파일러는 이 경우가 없다고 가정
// 컴파일러 최적화 결과 (GCC -O2)// if (x + 1 > x) → 항상 true로 최적화됨// 부호 없는 정수는 오버플로우가 정의됨 (모듈러 산술)unsigned int u = UINT_MAX;unsigned int v = u + 1; // 정의됨: 0
// 안전한 오버플로우 검사bool safe_add(int a, int b, int* result){ // C++20 return !__builtin_add_overflow(a, b, result); // 또는 직접 검사: // if (b > 0 && a > INT_MAX - b) return false; // if (b < 0 && a < INT_MIN - b) return false; // *result = a + b; return true;}2. 배열 범위 초과 접근
섹션 제목: “2. 배열 범위 초과 접근”int arr[5] = {1, 2, 3, 4, 5};
// UB: 배열 범위 초과int x = arr[5]; // UBint y = arr[-1]; // UB
// 포인터 산술 UBint* p = arr + 5; // 하나 지난 포인터: 정의됨int z = *p; // UB: 역참조는 불가
// 안전 대안std::array<int, 5> safe_arr = {1, 2, 3, 4, 5};int w = safe_arr.at(5); // std::out_of_range 예외3. 허상 포인터와 Use-After-Free
섹션 제목: “3. 허상 포인터와 Use-After-Free”int* make_local(){ int x = 42; return &x; // UB: 지역 변수 주소 반환}
void use_after_free(){ int* p = new int(10); delete p; *p = 20; // UB: 해제된 메모리 접근 delete p; // UB: 이중 해제}
// nullptr 역참조void null_deref(){ int* p = nullptr; *p = 5; // UB}4. 엄격한 앨리어싱 규칙 (Strict Aliasing)
섹션 제목: “4. 엄격한 앨리어싱 규칙 (Strict Aliasing)”// 다른 타입 포인터를 통한 접근 → UBfloat f = 3.14f;int* i = reinterpret_cast<int*>(&f); // UB: float를 int로 앨리어싱*i = 0;
// 올바른 타입 펀닝: memcpy 사용uint32_t bits;std::memcpy(&bits, &f, sizeof(f)); // 정의됨
// C++20: std::bit_castuint32_t bits2 = std::bit_cast<uint32_t>(f); // 정의됨, constexpr 가능
// char/unsigned char는 예외: 어떤 타입이든 앨리어싱 허용unsigned char* raw = reinterpret_cast<unsigned char*>(&f); // 정의됨5. 초기화되지 않은 변수
섹션 제목: “5. 초기화되지 않은 변수”int x; // 초기화되지 않음int y = x + 1; // UB: x 값이 미정
bool flag;if (flag) { } // UB: flag가 0도 1도 아닐 수 있음
// 컴파일러는 flag가 항상 false라고 최적화할 수 있음
// 안전: 항상 초기화int a = 0;bool b = false;int arr[10] = {}; // 0으로 초기화6. 시프트 연산 UB
섹션 제목: “6. 시프트 연산 UB”int x = 1;
// 음수 시프트 → UBint y = x << -1; // UB
// 타입 너비 이상 시프트 → UB (32비트 int 기준)int z = x << 32; // UBint w = x << 31; // UB (부호 있는 오버플로우)
// 안전한 시프트uint32_t u = 1u;uint32_t v = u << 31; // 정의됨: 부호 없는 타입
// 또는 std::rotl 사용 (C++20)uint32_t rotated = std::rotl(u, 31);7. 컴파일러 UB 최적화 예시
섹션 제목: “7. 컴파일러 UB 최적화 예시”// 컴파일러가 UB를 이용해 코드를 제거하는 예void process(int* p){ // 컴파일러: p가 UB 없이 역참조되므로 p != nullptr *p = 42; if (p == nullptr) { // 이 분기는 제거됨! handle_null(); }}
// 루프 무한화 예시for (int i = 0; i < n; ++i){ // i + 1이 오버플로우하면 UB → 컴파일러는 오버플로우 없다고 가정 // → 루프 조건을 항상 true로 최적화할 수 있음 arr[i + 1] = arr[i];}8. UB 탐지 도구
섹션 제목: “8. UB 탐지 도구”# UBSan — 미정의 동작 탐지clang++ -fsanitize=undefined -g -O1 main.cpp -o main./main# runtime error: signed integer overflow: 2147483647 + 1
# ASan — 메모리 오류 탐지 (Use-after-free, OOB)clang++ -fsanitize=address -g -O1 main.cpp -o main./main
# 두 가지 동시 적용clang++ -fsanitize=address,undefined -g -O1 main.cpp -o main
# Valgrind — 메모리 누수, Use-after-freevalgrind --tool=memcheck --leak-check=full ./main
# MSVC: /RTC1 (런타임 검사), /analyze (정적 분석)cl /RTC1 /analyze main.cpp9. 컴파일러 경고 활성화
섹션 제목: “9. 컴파일러 경고 활성화”# GCC/Clang 경고 플래그-Wall -Wextra -Wpedantic-Wshadow-Wconversion-Wnull-dereference-Wformat=2-Wshift-overflow=2-fsanitize=undefined
# CMake 설정target_compile_options(myapp PRIVATE -Wall -Wextra -Wpedantic $<$<CONFIG:Debug>:-fsanitize=undefined,address>)target_link_options(myapp PRIVATE $<$<CONFIG:Debug>:-fsanitize=undefined,address>)UB의 핵심 교훈: 컴파일러는 UB가 일어나지 않는다고 가정하고 최적화한다. 따라서 디버그 빌드에서의 ‘우연한 정상 동작’을 믿지 마세요. 개발 중에는 -fsanitize=undefined,address를 항상 활성화하고, std::bit_cast로 타입 펀닝, std::array::at()으로 경계 검사, 정수 오버플로우는 부호 없는 타입이나 __builtin_*_overflow로 처리하세요.