미정의 동작(Undefined Behavior)란?

C 언어나 C++의 규격이 결과에 대해 아무것도 요구하지 않는 동작. 부호 있는 정수의 오버플로, 배열의 범위 밖 읽기/쓰기, NULL 포인터의 참조, 해제한 영역의 사용, 음수 값의 왼쪽 시프트 등이 해당한다. 컴파일러는 올바른 프로그램에서는 미정의 동작이 일어나지 않는다고 가정하고 최적화해도 되므로, 우연히 기대대로 동작하기도 하고, 최적화 설정에 따라 판정이 사라지는 등 예상하지 못한 결과가 되기도 한다. 처리계가 정하고 문서에 적는 '처리계 정의 동작'과는 구별된다.

일본어 명칭: 未定義動作

미정의 동작(Undefined Behavior)은(는) C 언어 기초, 컴파일러 등 2개 분야의 연습 문제 10개에 나와요.

분야: C 언어 기초

대표적인 예

부호 있는 정수 연산의 넘침(INT_MAX + 1 등), 배열의 범위 밖 읽기/쓰기, 초기화하지 않은 자동 변수의 값 사용, NULL 포인터나 댕글링 포인터를 통한 읽기/쓰기, 같은 영역의 이중 해제, 정수를 0으로 나누기, 형의 폭 이상의 시프트나 음수 값의 왼쪽 시프트 등이 있습니다. 부호 없는 정수의 넘침은 미정의 동작이 아니며, 2의 거듭제곱으로 나눈 나머지로 돌아간다고 정해져 있습니다.

컴파일러 최적화와의 관계

컴파일러는 '미정의 동작은 일어나지 않는다'고 가정해도 되므로, x + 1 > x를 항상 참으로 간주하거나 *p를 읽은 뒤의 p == NULL 판정을 삭제하기도 합니다. 따라서 -O0에서는 기대대로 동작하던 프로그램이 -O2로 하거나 컴파일러의 버전을 올리기만 해도 동작하지 않게 되는 경우가 있습니다. 이는 컴파일러의 오류가 아니라 프로그램이 규격의 전제를 어기고 있는 것이 원인입니다.

찾는 방법과 막는 방법

gcc나 clang에서는 -fsanitize=undefined(UBSan)를 붙이면 부호 있는 정수의 오버플로 등을, -fsanitize=address(ASan)를 붙이면 범위 밖 읽기/쓰기나 해제 후 사용을 실행 시에 검출할 수 있습니다. -Wall이나 -Wextra의 경고, 정적 분석 도구, MISRA C 같은 규약에 따른 작성 방식도 미정의 동작을 줄이는 데 도움이 됩니다.

도전해 보기: 미정의 동작(Undefined Behavior) 관련 문제

C 언어 규격에서 말하는 미정의 동작의 설명으로 올바른 것은?

  1. A규격이 아무것도 정하지 않은 동작으로, 컴파일러는 그것이 일어나지 않는다고 가정하고 최적화해도 되며, 예상하지 못한 결과가 되기도 한다
  2. B처리계마다 결과를 정하고, 그 결과를 문서에 적어 두는 것이 의무화되어 있는 동작
  3. C실행 시에 반드시 검출되어, 오류 메시지를 내고 프로그램이 멈추는 동작
정답과 해설 보기

정답: A. 규격이 아무것도 정하지 않은 동작으로, 컴파일러는 그것이 일어나지 않는다고 가정하고 최적화해도 되며, 예상하지 못한 결과가 되기도 한다

미정의 동작은 부호 있는 정수의 오버플로, 배열의 범위 밖 참조, NULL 포인터의 참조, 해제 후 사용 등과 같이 규격이 아무것도 요구하지 않는 동작입니다. 우연히 기대대로 동작하기도 하지만, 컴파일러는 '올바른 프로그램에서는 일어나지 않는다'고 가정하고 최적화해도 되므로, 최적화 설정이나 컴파일러의 버전을 바꾸기만 해도 결과가 바뀌는 경우가 있습니다. 이에 비해 int의 크기나 char의 부호 유무처럼 처리계가 정하고 문서에 적어야 하는 것을 처리계 정의 동작이라고 합니다. gcc나 clang에서는 -fsanitize=undefined를 붙이면 일부 미정의 동작을 실행 시에 검출할 수 있습니다.

int와 unsigned int는 32비트로 한다. 다음 중 C 언어 규격에서 미정의 동작이 되는 것은?

  1. Aunsigned int 변수의 값이 최댓값 4294967295일 때 1을 더한다
  2. Bint의 음수 값을 >>로 오른쪽으로 1비트 시프트한다
  3. Cint 변수의 값이 최댓값 2147483647일 때 1을 더한다
정답과 해설 보기

정답: C. int 변수의 값이 최댓값 2147483647일 때 1을 더한다

int의 2147483647 + 1처럼 부호 있는 정수 연산의 결과가 나타낼 수 있는 범위를 넘는 것은 미정의 동작입니다. 부호 없는 정수의 연산은 2³²로 나눈 나머지로 돌아간다고 정해져 있으므로, 4294967295 + 1은 0이 되며 미정의 동작이 아닙니다. 음수 값의 오른쪽 시프트는 산술 시프트가 될지 논리 시프트가 될지를 처리계가 정하는 처리계 정의 동작으로, gcc에서는 산술 시프트가 됩니다. 한편, 음수 값의 왼쪽 시프트는 미정의 동작입니다.

자주 묻는 질문

미정의 동작과 처리계 정의 동작은 무엇이 다른가요?

처리계 정의 동작은 int의 크기, char의 부호 유무, 음수 값의 오른쪽 시프트 결과처럼 처리계가 하나의 동작을 골라 그것을 문서에 적어 두는 것입니다. 미정의 동작은 규격이 아무것도 요구하지 않는 것으로, 같은 처리계에서도 상황에 따라 결과가 달라져도 상관없습니다. 이식성 있는 프로그램에서는 처리계 정의 동작에 의존하는 부분을 명시하고, 미정의 동작은 일으키지 않도록 합니다.

미정의 동작이라도 동작하고 있다면 문제없나요?

문제가 있습니다. 지금의 환경에서 우연히 기대대로 동작하더라도, 최적화 설정, 컴파일러의 버전, 주변 코드가 바뀌기만 해도 다른 결과가 되는 경우가 있습니다. 보안 측면에서도 범위 밖 쓰기 같은 미정의 동작은 공격에 악용될 우려가 있습니다.

해설 속 미정의 동작(Undefined Behavior)

미정의 동작(Undefined Behavior)이(가) 나오는 문제 (10)

  1. C 언어에서 부호 있는 정수의 오버플로는 미정의 동작이다. 이 전제에서 위의 함수를 최적화하는 컴파일러가 생성할 수 있는 코드로 올바른 것은?
  2. C 언어 규격에서 말하는 미정의 동작의 설명으로 올바른 것은?
  3. int와 unsigned int는 32비트로 한다. 다음 중 C 언어 규격에서 미정의 동작이 되는 것은?
  4. uint8_t는 8비트의 부호 없는 정수형이다. x를 10번 증가시키는 위의 코드를 실행했을 때의 출력으로 올바른 것은?
  5. int는 32비트(최댓값 2147483647)로 한다. 위의 함수 fact(n)이 n의 계승을 오버플로 없이 올바르게 반환하는 최대의 n은?
  6. 위의 함수 make가 반환하는 포인터에 대해 올바른 것은?
  7. C 언어에서 free(p); 바로 다음에 p = NULL;이라고 쓰는 것이 권장되는 이유로 가장 적절한 것은?
  8. C 언어에서 NULL 포인터를 참조했을(p가 NULL일 때 *p를 읽었을) 때의 설명으로 올바른 것은?
  9. #define으로 정의한 매크로 SQUARE를 사용하는 위의 코드를 실행했을 때의 출력으로 올바른 것은?
  10. 위의 함수를 gcc로 최적화를 켜고 컴파일하면 if (p == NULL)의 판정이 삭제되는 경우가 있다. 그 이유로 올바른 것은?

관련 용어

포인터(Pointer) 변수(Variable) 배열(Array) 산술 시프트(Arithmetic Shift) 재귀(Recursion) 스택(Stack) 댕글링 포인터(Dangling Pointer) 동적 메모리 할당(Dynamic Memory Allocation) 페이지 폴트(page fault) 마이컴(마이크로컨트롤러)

용어집으로 돌아가기