미정의 동작

컴퓨터 프로그래밍에서 프로그램은 프로그래밍 언어 사양이 특정 요구 사항을 강제하지 않는 코드를 포함하거나 실행할 때 **미정의 동작**(undefined behavior, **UB**)을 보인다.access-date=23 February 2025 이는 언어 사양이 결과를 규정하지 않는 미지정 동작과, 컴퓨팅 플랫폼의 다른 구성 요소(예: ABI 또는 트랜슬레이터 문서)의 문서를 따르는 구현 정의 동작과는 다르다. C 프로그래밍 커뮤니티에서 미정의 동작은 컴파일러가 "코에서 악마가 날아가게" 하는 등 원하는 모든 것을 할 수 있도록 허용한다고 설명한 comp.std.c 게시물에 따라 유머러스하게 "**코 악마**"라고 불리기도 한다.access-date=12 June 2014
# 개요
일부 프로그래밍 언어는 프로그램 실행 중에 미정의 동작이 발생하지 않는 한, 소스 코드와 다른 방식으로 작동하거나 심지어 다른 제어 흐름을 가질 수 있도록 허용한다. 미정의 동작은 프로그램이 충족해서는 안 되는 조건 목록의 이름이다. C의 초기 버전에서 미정의 동작의 주요 이점은 다양한 기계에 대해 성능이 좋은 컴파일러를 생성한다는 것이었다. 특정 구문은 기계별 기능에 매핑될 수 있었고, 컴파일러는 언어에 의해 부과된 의미론과 일치하도록 부작용을 조정하기 위해 런타임에 추가 코드를 생성할 필요가 없었다. 프로그램 소스 코드는 특정 컴파일러와 지원할 컴퓨팅 플랫폼에 대한 사전 지식을 가지고 작성되었다. 그러나 플랫폼의 점진적인 표준화로 인해 이러한 이점은 특히 C의 최신 버전에서 줄어들었다. 이제 미정의 동작 사례는 일반적으로 코드의 모호하지 않은 소프트웨어 버그를 나타내는데, 예를 들어 배열의 범위를 벗어나 인덱싱하는 경우이다. 정의에 따르면 런타임은 미정의 동작이 결코 발생하지 않는다고 가정할 수 있으므로, 일부 유효하지 않은 조건을 확인할 필요가 없다. 컴파일러의 경우, 이는 또한 다양한 프로그램 변환이 유효해지거나, 그 정확성 증명이 단순화된다는 것을 의미한다. 이는 프로그램 상태가 그러한 조건을 결코 충족하지 않는다는 가정에 의존하는 다양한 종류의 최적화를 가능하게 한다. 컴파일러는 프로그래머에게 알리지 않고 소스 코드에 있었을 수 있는 명시적 검사를 제거할 수도 있다. 예를 들어, 발생 여부를 테스트하여 미정의 동작을 감지하는 것은 정의상 작동이 보장되지 않는다. 이로 인해 휴대용 오류 방지 옵션을 프로그래밍하기 어렵거나 불가능하다(일부 구문의 경우 비휴대용 솔루션이 가능하다). 현재 컴파일러 개발은 일반적으로 범용 데스크톱 및 랩톱 시장(예: amd64)에서 주로 사용되는 플랫폼에서도 마이크로 최적화를 중심으로 설계된 벤치마크로 컴파일러 성능을 평가하고 비교한다. 따라서 미정의 동작은 컴파일러 성능 향상을 위한 충분한 여지를 제공하는데, 특정 소스 코드 문의 소스 코드가 런타임에 어떤 것이든 매핑될 수 있기 때문이다. C 및 C++의 경우, 컴파일러는 이러한 경우에 컴파일 시간 진단을 제공할 수 있지만 필수는 아니다. 구현은 디지털 논리의 Don't-care term과 유사하게 이러한 경우에 무엇을 하든 올바른 것으로 간주된다. 컴파일러 구현이 이러한 경우에 진단을 발행할 수 있지만, 미정의 동작을 호출하지 않는 코드를 작성하는 것은 프로그래머의 책임이다. 오늘날 컴파일러는 이러한 진단을 활성화하는 플래그를 가지고 있다. 예를 들어, `-fsanitize=undefined`는 gcc 4.9GCC Undefined Behavior Sanitizer – ubsan 및 클랭에서 "미정의 동작 새니타이저"(UBSan)를 활성화한다. 그러나 이 플래그는 기본값이 아니며, 이를 활성화하는 것은 코드를 빌드하는 사람의 선택이다. 특정 상황에서는 미정의 동작에 대한 특정 제한이 있을 수 있다. 예를 들어, CPU의 명령어 집합 사양은 일부 명령어 형식의 동작을 미정의로 둘 수 있지만, CPU가 메모리 보호를 지원하는 경우 사양에는 사용자 접근 가능한 어떤 명령어("user-accessible instruction")도 운영체제의 보안에 구멍을 낼 수 없다는 일반적인 규칙이 포함될 것이다. 따라서 실제 CPU는 그러한 명령어에 응답하여 사용자 레지스터를 손상시키는 것이 허용되지만, 예를 들어 관리자 모드로 전환하는 것은 허용되지 않는다. 툴체인 또는 런타임이 소스 코드에서 발견되는 특정 구문이 런타임에 사용 가능한 특정 잘 정의된 메커니즘에 매핑된다고 명시적으로 문서화하는 경우, 런타임 플랫폼도 미정의 동작에 대한 일부 제한 또는 보장을 제공할 수 있다. 예를 들어, 인터프리터는 언어 사양에서 미정의된 일부 작업에 대해 특정 동작을 문서화할 수 있지만, 동일한 언어의 다른 인터프리터 또는 컴파일러는 그렇지 않을 수 있다. 컴파일러는 특정 ABI를 위한 실행 코드를 생성하여 컴파일러 버전에 따라 semantic gap을 채운다. 해당 컴파일러 버전의 문서와 ABI 사양은 미정의 동작에 대한 제한을 제공할 수 있다. 이러한 구현 세부 사항에 의존하면 소프트웨어는 휴대성이 없게 되지만, 소프트웨어가 특정 런타임 외부에서 사용되지 않아야 한다면 휴대성은 문제가 되지 않을 수 있다. 미정의 동작은 프로그램 충돌을 일으킬 수 있으며, 데이터의 침묵 손실 및 잘못된 결과 생성과 같이 감지하기 더 어렵고 프로그램이 정상적으로 작동하는 것처럼 보이게 하는 오류를 발생시킬 수도 있다.
## 오류 프로그램
프로그래밍 언어 설계에서 **오류 프로그램**은 의미론이 잘 정의되지 않았지만, 언어 구현이 컴파일 시간 또는 실행 시간 모두에서 오류를 알릴 의무가 없는 프로그램이다. 예를 들어 에이다에서는 다음과 같다. :바운드 오류 외에도 언어 규칙은 특정 종류의 오류가 오류 실행으로 이어진다고 정의한다. 바운드 오류와 마찬가지로, 구현은 런타임 이전 또는 도중에 그러한 오류를 감지할 필요가 없다. 바운드 오류와 달리, 오류 실행의 가능한 효과에 대한 언어 지정 바운드는 없으며, 그 효과는 일반적으로 예측할 수 없다.chapter-url=https://www.adaic.org/resources/add_content/standards/95lrm/ARM_HTML/RM-1-1-5.html 조건을 "오류"로 정의하는 것은 언어 구현이 잠재적으로 비용이 많이 드는 검사(예: 전역 변수가 서브루틴 매개변수와 동일한 객체를 참조하는지 여부)를 수행할 필요는 없지만, 프로그램의 의미론을 정의할 때 조건이 참이라는 것에 의존할 수 있음을 의미한다.
# 장점
작업을 미정의 동작으로 문서화하면 컴파일러가 이 작업이 규칙을 준수하는 프로그램에서는 결코 발생하지 않을 것이라고 가정할 수 있다. 이는 컴파일러에 코드에 대한 더 많은 정보를 제공하며, 이 정보는 더 많은 최적화 기회로 이어질 수 있다. C 언어의 예시: int foo(unsigned char x) { int value = 2147483600; /* assuming 32-bit int and 8-bit char */ value += x; if (value 60) { run_one_task(ptrx, z); } } 컴파일러는 값 범위 분석을 적용하여 여기에서 `while` 루프를 최적화할 수 있다. `foo()`를 검사하여 `ptrx`가 가리키는 초기 값이 47을 초과할 수 없다는 것을 안다(47을 초과하면 `foo()`에서 미정의 동작을 유발할 수 있기 때문이다). 따라서 `*ptrx > 60`의 초기 검사는 규칙을 준수하는 프로그램에서 항상 거짓이 될 것이다. 더 나아가, `z` 결과가 이제 사용되지 않고 `foo()`에 부작용이 없으므로, 컴파일러는 `run_tasks()`를 즉시 반환하는 빈 함수로 최적화할 수 있다. `foo()`가 별도로 컴파일된 객체 파일에 정의되어 있다면 `while` 루프의 사라짐은 특히 놀라울 수 있다. 부호 있는 정수 오버플로를 미정의로 허용하는 또 다른 이점은 변수의 값을 소스 코드의 변수 크기보다 큰 프로세서 레지스터에 저장하고 조작할 수 있다는 것이다. 예를 들어, 소스 코드에 지정된 변수의 타입이 기본 레지스터 너비보다 좁은 경우(예: 64비트 머신에서 `int`, 일반적인 시나리오), 컴파일러는 코드의 정의된 동작을 변경하지 않고 생성하는 기계어에서 해당 변수에 대해 부호 있는 64비트 정수를 안전하게 사용할 수 있다. 프로그램이 32비트 정수 오버플로 동작에 의존했다면, 컴파일러는 64비트 머신용으로 컴파일할 때 추가 로직을 삽입해야 했을 것이다. 왜냐하면 대부분의 기계 명령어의 오버플로 동작은 레지스터 너비에 따라 달라지기 때문이다.title = A bit of background on compilers exploiting signed overflow 미정의 동작은 또한 컴파일러와 정적 프로그램 분석 모두에서 더 많은 컴파일 시간 검사를 허용한다.
# 위험
C 및 C++ 표준에는 미정의 동작의 여러 형태가 있으며, 이는 컴파일러 구현의 자유도를 높이고 컴파일 시간 검사를 가능하게 하지만, 존재할 경우 미정의 런타임 동작을 초래한다. 특히 ISO C 표준에는 미정의 동작의 일반적인 원인을 나열한 부록이 있다.ISO/IEC 9899:2011 §J.2. 더욱이 컴파일러는 미정의 동작에 의존하는 코드를 진단할 의무가 없다. 따라서 프로그래머, 심지어 숙련된 프로그래머조차도 실수로 또는 단순히 수백 페이지에 달하는 언어 규칙에 익숙하지 않아 미정의 동작에 의존하는 경우가 흔하다. 이로 인해 다른 컴파일러 또는 다른 설정이 사용될 때 드러나는 버그가 발생할 수 있다. 클랭 새니타이저와 같은 동적 미정의 동작 검사를 활성화하여 테스트 또는 퍼징을 수행하면 컴파일러 또는 정적 분석기가 진단하지 못하는 미정의 동작을 포착하는 데 도움이 될 수 있다.url=https://www.youtube.com/watch?v=v1COuU2vU_w 미정의 동작은 소프트웨어의 보안 취약점으로 이어질 수 있다. 예를 들어, 주요 웹 브라우저의 버퍼 오버플로 및 기타 보안 취약점은 미정의 동작으로 인한 것이다. 2008년 GCC 개발자들이 미정의 동작에 의존하는 특정 오버플로 검사를 생략하도록 컴파일러를 변경했을 때, CERT는 최신 버전의 컴파일러에 대해 경고를 발행했다.publisher=CERT Linux Weekly News는 PathScale C, 마이크로소프트 비주얼 C++ 2005 및 여러 다른 컴파일러에서도 동일한 동작이 관찰되었다고 지적했다.website=Linux Weekly News 이 경고는 나중에 다양한 컴파일러에 대해 경고하도록 수정되었다.publisher=CERT
# C 및 C++의 예시
C에서 미정의 동작의 주요 형태는 크게 다음과 같이 분류할 수 있다.author= Pascal Cuoq and John Regehr 공간 메모리 안전 위반, 시간 메모리 안전 위반, 정수 오버플로, 엄격한 별칭 위반, 정렬 위반, 순서 없는 수정, 데이터 경합, 그리고 I/O를 수행하지도 종료하지도 않는 루프. C에서 자동 변수를 초기화하기 전에 사용하면 미정의 동작이 발생하며, 정수 0으로 나누기, 부호 있는 정수 오버플로, 정의된 범위 밖의 배열 인덱싱(버퍼 오버플로 참조) 또는 널 포인터 역참조도 마찬가지이다. 일반적으로 미정의 동작의 모든 인스턴스는 추상 실행 기계를 알 수 없는 상태로 남겨두고 전체 프로그램의 동작을 미정의 상태로 만든다. 문자열 리터럴이 일반적으로 읽기 전용 메모리에 저장되기 때문에, 이를 수정하려고 시도하면 미정의 동작이 발생한다.ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages – C++ §2.13.4 String literals [lex.string] para. 2 char* p = "wikipedia"; // valid C, deprecated in C++98/C++03, ill-formed as of C++11 p[0] = 'W'; // undefined behavior 정수 0으로 나누기는 미정의 동작을 발생시킨다.ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages – C++ §5.6 Multiplicative operators [expr.mul] para. 4 int x = 1; return x / 0; // undefined behavior 특정 포인터 연산은 미정의 동작을 초래할 수 있다.ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §5.7 Additive operators [expr.add] para. 5 int arr[4] = {0, 1, 2, 3}; int* p = arr + 5; // undefined behavior for indexing out of bounds p = nullptr; int a = *p; // undefined behavior for dereferencing a null pointer C 및 C++에서 객체에 대한 포인터의 관계 비교(보다 작음 또는 보다 큼 비교)는 포인터가 동일한 객체의 멤버 또는 동일한 배열의 요소를 가리키는 경우에만 엄격하게 정의된다.ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages – C++ §5.9 Relational operators [expr.rel] para. 2 예시: int main(void) { int a = 0; int b = 0; return &a; >`의 오른쪽 피연산자)를 다음 범위 내에 유지하는 것이다: [0, sizeof value * CHAR_BIT - 1] (여기서 `value`는 왼쪽 피연산자이다). int num = -1; unsigned int val = 1 &T; { unsafe { zeroed() } } `null_ref`를 호출하는 것은 모든 참조 타입에 의해 부과되는 불변성 때문에 항상 잘못 형성되는데, 함수 자체가 `unsafe fn` 항목이 아니고 안전한 코드에서 호출될 수 있음에도 불구하고 그렇다. 또한, 어떤 널 포인터를 역참조하는 것도 미정의이지만, 많은 호스트 시스템은 여전히 세그멘테이션 오류에서 이러한 경우를 처리하도록 설계되어 있다. use std::ptr::null; fn main() { let p: *const i32 = null(); // SAFETY: `p` is null and may not be dereferenced. unsafe { *p }; }
# 더 읽어보기
* 피터 반 데르 린덴, Expert C Programming. * UB Canaries (April 2015), 존 레거 (유타 대학교, 미국) * Undefined Behavior in 2017 (July 2017) 파스칼 쿠오 (TrustInSoft, 프랑스) 및 존 레거 (유타 대학교, 미국)