- 본 글에서는 표준에서 정의되지 않은 행동인 implementation-defined behavior, unspecified behavior, undefined behavior를 소개한다.
- 해당 용어들은 코딩 규칙에서 자주 언급된다.
표준 문서에서 정의하는 behavior 종류
- 기본적으로 C언어 컴파일러는 코드에서의observable behavior(예상되는 행동에 따른 결과)를 명확하게 정의한다.
ill-formed, ill-formed no diagnostic required
- 그러나 몇몇 경우에선 표준이 observable behavior를 정의하지 않고 즉, 표준에서 정의되지 않고 컴파일러에 의해 구현된다.
Implementation-defined behavior, Unspecified behavior, Undefined behavior
- 이 중 개발자가 주의해야 하는 behavior는 하단의 ‘표준에서 정의되지 않음’ 항목이다.
- MISRA C 2012에서는 프로그램 실행 중 발생하는 implementation-defined behavior(Dir 1.1)는 문서화 되어 개발자가 이를 이해하고 있어야 하며, unspecified or undefined behavior(Rule 1.3)가 발생하면 안된다고 명시되어 있다.
출처: https://en.cppreference.com/w/cpp/language/ub
https://www.secmem.org/blog/2020/01/17/c-c++-and-ub/
ISO/IEC 9899:201x Committee Draft
MISRA C 2012 가이드라인과 CWE(Common Weakness Enumeration)
- C언어에서 발생하는 undefined behavior는 개발자가 프로그램 결과를 예측 할 수 없으며, 이는 CVE(Common Vulnerabilities and Exposures) 취약점을 발생시킨다. CVE 취약점은 undefined behavior 이외에도 컴파일러 최적화 옵션을 통해서도 발생할 수 있다.
- MISRA C 2012 가이드라인과 CWE(Common Weakness Enumeration) 가이드라인을 모두 준수하면 CVE 취약점을 보완할 수 있다. CWE 가이드라인은 CVE 가이드라인을 모두 포함하고, MISRA C 2012 가이드라인은 CWE 가이드라인을 일부 포함한다.
ill-formed
- 프로그램이 syntax error(문법 오류)나 semantic error(개발자 의도와 다르게 실행되는 오류) 등이 있어 컴파일러가 이에 대한 에러/경고 메시지를 출력하는 경우다.
ex) 배열 길이에서 변수 대입
1
2
int size = 5;
int arr[size];
ill-formed no diagnostic required
- ill-formed나 컴파일러가 감지할 수 없는 경우다.
- 링크 과정에서 에러를 감지할 수 있으며, 실행 시의 행동은 정의되지 않는다.
ex) One Definition Rule (ODR) 위배
ODR이란 translation unit에서 함수, 객체, 변수 등의 식별자가 유일해야 하는 규칙이다.
출처: http://egloos.zum.com/parkpd/v/3488517
Implementation-defined behavior
- Unspecified behavior where each implementation documents how the choice is made
- 동작에 대한 결과가 문서화 되어있다.
- 해당 기능이 컴파일러의 구현 방법에 따라 정의된 행동으로, 컴파일러 마다 다른 동작을 할 수 있다.
ex) 우측 쉬프트 연산의 상위 비트 확장 방법, int 자료형의 크기
Unspecified behavior
- use of an unspecified value, or other behavior where this International Standard provides two or more possibilities and imposes no further requirements on which is chosen in any instance
- 동작에 대한 구현은 제공하지만 문서화 되어있지 않다.
- 해당 기능이 컴파일러의 구현 방법에 따라 정의된 행동으로, 컴파일러 마다 다른 동작을 할 수는 있지만 일반적으로 예상 가능한 유효 범위 내에서 동작한다.
ex) malloc 함수 인자로 0을 주고, 실행할 때 메모리가 얼마나 할당되는가에 대한 명시가 되어 있지 않음.
ex) function_call(foo(), bar()); 구현체에 따라 foo() 함수가 먼저 호출 될 수도 bar() 함수가 먼저 호출 될 수 도 있음.
Undefined behavior
- behavior, upon use of a non-portable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements
- 표준에서 정의되지 않은 모든 행동이다.
- 어떤 결과가 나올지 정의되어 있지 않으며, 예상치 못한 행동이 발생할 수 있다. 운이 좋으면 프로그래머가 의도한 방향으로 컴파일 될 수도 있지만, 실행 중 에러를 발생시킬 수 있다.
- Undefined behavior의 결과는 누구도 예측할 수 없기 때문에 반드시 피해야 한다.
ex)
초기화가 안 된 변수의 사용
부호 있는 정수형의 overflow
부호 있는 정수 자료형의 왼쪽/오른쪽 시프트
범위를 벗어난 배열 접근
유효하지 않은 포인터의 역참조
Undefined behavior 예제
- 32비트 int 형의 최대 값인 2,147,483,64을 초과하면 오버플로우가 발생하여, value는 음수가 되고 bar() 함수가 호출된다.
- 하지만 이는 개발자의 예상이며, 컴파일러는 이를 다르게 해석 할 수 있다.
1
2
3
4
5
6
7
8
int foo(unsigned char x)
{
int value = 2147483600; /* assuming 32 bit int */
value += x;
if (value < 2147483600)
bar();
return value;
}
- 오버플로우가 발생하는 경우, 컴파일러는 코드에 최적화를 다음과 같이 진행할 수 있다.
- Undefined behavior는 컴파일러가 어떻게 컴파일하는가에 따라서 다르게 행동할 수 있다.
1
2
3
4
5
6
int foo(unsigned char x)
{
int value = 2147483600; /* assuming 32 bit int */
value += x;
return value;
}
Comments powered by Disqus.