수색…


소개

C에서는 일부 표현식에서 정의되지 않은 동작이 발생 합니다. 표준은 컴파일러가 그러한 표현식을 만났을 때 어떻게 행동해야 하는지를 명시 적으로 정의하지 않도록 명시 적으로 선택합니다. 결과적으로 컴파일러는 적합하다고 생각되는 것을 자유롭게 수행 할 수 있으며 유용한 결과, 예상치 못한 결과 또는 충돌을 일으킬 수 있습니다.

UB를 호출하는 코드는 특정 컴파일러가있는 특정 시스템에서 의도 한대로 작동하지만 다른 시스템이나 다른 컴파일러, 컴파일러 버전 또는 컴파일러 설정으로는 작동하지 않을 수 있습니다.

비고

정의되지 않은 행동 (UB)이란 무엇입니까?

정의되지 않은 동작 은 C 표준에서 사용되는 용어입니다. C11 표준 (ISO / IEC 9899 : 2011)에서는 정의되지 않은 동작이라는 용어를 다음과 같이 정의합니다.

이식 할 수 없거나 오류가있는 프로그램 구성이나 오류가있는 데이터를 사용할 때이 국제 표준이 요구 사항을 부과하지 않는 행동

내 코드에 UB가 있으면 어떻게됩니까?

다음은 표준에 따라 정의되지 않은 동작으로 인해 발생할 수있는 결과입니다.

비고 정의되지 않은 행동은 예측할 수없는 결과로 상황을 완전히 무시하고 환경의 문서화 된 방식으로 번역 또는 프로그램 실행 중에 (진단 메시지 발급 여부와 상관없이) 행동하거나 번역 또는 실행을 종료 할 때까지 진단 메시지 발행).

다음 인용문은 정의되지 않은 동작에서 발생하는 결과 (형식적으로는 적지 만)를 설명하는 데 자주 사용됩니다.

"컴파일러가 [주어진 정의되지 않은 구조체]를 만날 때 악마가 코에서 빠져 나오게하는 것은 합법적입니다."(컴파일러가 ANSI C 표준을 위반하지 않고 코드를 해석하는 임의의 기괴한 방법을 선택할 수 있음)

UB가있는 이유는 무엇입니까?

그렇게 나쁘다면 왜 정의하거나 구현 정의로 정의하지 않았을까요?

정의되지 않은 동작은 최적화를위한 더 많은 기회를 제공합니다. 컴파일러는 모든 코드가 정의되지 않은 동작을 포함하지 않는다고 가정 할 수 있으므로 런타임 검사를 피하고 다른 방법을 증명하는 것이 값 비싸거나 불가능한 최적화를 수행 할 수 있습니다.

UB가 추적하기 어려운 이유는 무엇입니까?

정의되지 않은 동작으로 인해 발견하기 어려운 버그가 생성되는 이유는 최소한 두 가지입니다.

  • 컴파일러는 정의되지 않은 동작에 대해 경고하지 않아도되며 일반적으로 신뢰할 수는 없습니다. 실제로 그것을 요구하는 것은 정의되지 않은 행동의 존재 이유에 대해 직접적으로 갈 것입니다.
  • 예측할 수없는 결과는 작동이 정의되지 않은 구조가 발생하는 정확한 지점에서 펼쳐지기 시작할 수 없습니다. 정의되지 않은 동작은 전체 실행을 오염시키고 그 효과는 언제라도 발생할 수 있습니다. 정의되지 않은 구문의 실행 중, 실행 후 또는 실행 전에 발생 합니다.

널 포인터 참조 해제 고려 : 컴파일러는 널 포인터 참조 해제를 진단 할 필요가 없으며 실행시에 함수에 전달 된 포인터가 없거나 전역 변수가 null 일 수도 있습니다. 그리고 널 포인터 역 참조가 발생할 때, 표준은 프로그램이 충돌 할 필요가 있음을 명령하지 않습니다. 오히려 프로그램이 더 일찍 중단되거나, 나중에 충돌하거나 충돌하지 않을 수 있습니다. 마치 널 포인터가 유효한 객체를 가리키고 완전히 정상적으로 동작하고 다른 상황에서는 충돌하는 것처럼 동작 할 수도 있습니다.

null 포인터 역 참조의 경우 C 언어는 null 포인터 역 참조 동작이 정의 되는 Java 또는 C #과 같은 관리되는 언어와 다릅니다. 정확한 시간에 예외가 throw됩니다 (Java의 경우 NullPointerException , C #의 경우 NullReferenceException ) 따라서 Java 또는 C #에서 오는 메시지는 진단 메시지 발행 여부와 상관없이 C 프로그램이 중단되어야한다고 잘못 생각할 수 있습니다.

추가 정보

명확하게 구분해야하는 몇 가지 상황이 있습니다.

  • 명시 적으로 정의되지 않은 동작, 즉 C 표준이 명시 적으로 사용자가 오프 한계라고 알려주는 곳입니다.
  • 암시 적으로 정의되지 않은 동작. 프로그램에 가져온 상황에 대한 동작을 예견하는 표준 텍스트가없는 곳입니다.

또한 많은 곳에서 컴파일러와 라이브러리 구현자가 자신의 정의를 내놓을 수있는 여지를 남겨두기 위해 특정 구문의 동작을 의도적으로 C 표준에서 정의하지 않는다는 점도 명심하십시오. 좋은 예는 POSIX 운영 체제 표준과 같은 C에 대한 확장이 훨씬 더 정교한 규칙을 정의하는 신호 및 신호 처리기입니다. 이 경우 플랫폼의 문서를 확인하기 만하면됩니다. C 표준은 당신에게 아무 것도 말할 수 없습니다.

또한 프로그램에서 정의되지 않은 동작이 발생하면 정의되지 않은 동작이 발생하는 지점 만 문제가되는 것이 아니라 전체 프로그램이 무의미 해지는 것을 의미합니다.

이러한 우려 때문에 C에서 프로그래밍하는 사람이 정의되지 않은 동작을 유발하는 일에 적어도 익숙해야한다는 것이 중요합니다 (특히 컴파일러가 UB에 대해 항상 경고하지는 않기 때문에).

정의되지 않은 동작을 감지하는 데 도움이되는 몇 가지 도구 (예 : PC-Lint와 같은 정적 분석 도구)가 있지만 아직 정의되지 않은 동작이 모두 감지되지는 않습니다.

널 포인터를 역 참조

이것은 정의되지 않은 동작을 일으키는 NULL 포인터를 역 참조하는 예제입니다.

int * pointer = NULL;
int value = *pointer; /* Dereferencing happens here */

NULL 포인터는 C 표준에 의해 유효하지 않은 객체를 가리키는 모든 포인터를 비교하고, 참조를 해제하면 정의되지 않은 동작을 호출합니다.

두 시퀀스 포인트간에 오브젝트를 두 번 이상 수정

int i = 42;
i = i++; /* Assignment changes variable, post-increment as well */
int a = i++ + i--;

이와 같은 코드는 종종 i 의 "결과 값"에 대한 추측을 유도합니다. 그러나 결과를 지정하는 대신 C 표준은 이러한 표현식을 평가할 때 정의되지 않은 동작이 생성되도록 지정합니다. C2011 이전에는이 ​​표준이 소위 시퀀스 포인트 라는 측면에서 이러한 규칙을 공식화했습니다.

이전 및 다음 시퀀스 포인트 사이에서 스칼라 객체는 저장된 값을 최대 한 번 표현식을 평가하여 수정해야합니다. 또한, 이전 값은 저장 될 값을 결정하기 위해서만 읽혀 져야한다.

(C99 표준, 섹션 6.5, 2 단락)

그 계획은 너무 조잡한 것으로 판명되었고, 일부 표현은 C99와 관련하여 정의되지 않은 행동을 나타냈다. C2011은 서열 점을 유지하지만, 서열화에 기초한이 영역에 대한보다 미묘한 접근 방식과 "전에 서열화 된"관계를 소개합니다 :

스. 라 오브젝트의 부작용이 동일한 스. 라 오브젝트의 다른 부작용 또는 동일한 스. 라 오브젝트의 값을 사용하는 값 ​​계산과 비교하여 순서가 지정되지 않은 경우, 동작은 정의되지 않습니다. 표현식의 서브 표현식에 허용 가능한 순서가 여러 개있는 경우 순서가 지정되지 않은 부작용이 발생하면 그 동작은 정의되지 않습니다.

(C2011 표준, 섹션 6.5, 2 단락)

"순서가있는 이전"관계의 전체 내용은 여기서 설명하기에는 너무 길지만 시퀀스 포인트를 대신하기보다는 시퀀스 포인트를 보완하기 때문에 이전에는 정의되지 않은 동작에 대한 동작을 정의하는 효과가 있습니다. 특히, 두 평가 사이에시 v 스 점이 있으면,시 v 스 점 앞에있는시 v 스는시 v 스 후 "시 v 스되기 전에"입니다.

다음 예제에는 잘 정의 된 동작이 있습니다.

int i = 42;
i = (i++, i+42); /* The comma-operator creates a sequence point */

다음 예제에는 정의되지 않은 동작이 있습니다.

int i = 42;
printf("%d %d\n", i++, i++); /* commas as separator of function arguments are not comma-operators */

정의되지 않은 동작의 어떤 형태와 마찬가지로 순차 규칙을 위반하는 표현식을 평가하는 실제 동작을 관찰하는 것은 회고 적 의미를 제외하고는 유익하지 않습니다. 언어 표준은 같은 프로그램의 미래 행동조차도 그러한 관찰이 예측 적이라고 기대할 근거가 없습니다.

값 반환 함수에 return 문이 없습니다.

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* Trying to use the (not) returned value causes UB */
  int value = foo();
  return 0;
}

함수가 값을 반환하도록 선언되면 함수를 통해 가능한 모든 코드 경로에서 함수를 반환해야합니다. 호출자 (반환 값이 필요함)가 반환 값 1 을 사용하려고하면 정의되지 않은 동작이 발생합니다.

정의되지 않은 동작은 호출자가 함수의 값을 사용하거나 액세스하려고 시도하는 경우 에만 발생합니다. 예를 들어,

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* The value (not) returned from foo() is unused. So, this program
   * doesn't cause *undefined behaviour*. */
  foo();
  return 0;
}
C99

main() 함수의 리턴 값 가정 때문에이 리턴 명령문없이 종료하는 것이 가능하다는 것이이 규칙에 대한 예외는 0 자동 케이스 (2)에 사용된다.


1 ( ISO / IEC 9899 : 201x , 6.9.1 / 12)

함수를 종료하는}에 도달하고 함수 호출의 값이 호출자에 의해 사용되면 동작은 정의되지 않습니다.

2 ( ISO / IEC 9899 : 201x , 5.1.2.2.3 / 1)

main 함수를 종료하는}에 도달하면 0 값이 반환됩니다.

부호있는 정수 오버플로

C99와 C11 둘 다 단락 6.5 / 5에 따라 결과가 표현식 유형의 표현 가능한 값이 아닌 경우 표현식을 평가하면 정의되지 않은 동작이 발생합니다. 산술 유형의 경우이를 오버플로 라고합니다. 단락 6.2.5 / 9가 적용되기 때문에 부호없는 정수 연산이 오버플로되지 않으므로 범위를 벗어나는 부호없는 결과가 범위 내 값으로 줄어 듭니다. 그러나 부호있는 정수 유형에는 유사한 조항이 없습니다. 이것들은 오버 플로우 할 수 있고 정의되지 않은 행동을 일으킨다. 예를 들어,

#include <limits.h>      /* to get INT_MAX */

int main(void) {
    int i = INT_MAX + 1; /* Overflow happens here */
    return 0;
}

이러한 유형의 정의되지 않은 동작의 대부분 인스턴스는 인식하거나 예측하기가 더 어렵습니다. 오버플로는 원칙적으로 피연산자에 대한 유효 경계가 없거나 피연산자 간의 관계가 존재하지 않는 부호가있는 정수 (통상적 인 산술 변환의 적용을 받음)에 대한 더하기, 빼기 또는 곱셈 연산에서 발생할 수 있습니다. 예를 들어,이 함수는 다음과 같습니다.

int square(int x) {
    return x * x;  /* overflows for some values of x */
}

합리적인 것이고, 작은 인수 값에 대해서는 올바른 일을하지만 큰 인수 값에 대해서는 그 동작이 정의되지 않습니다. 함수를 호출하는 프로그램이 결과로 정의되지 않은 동작을 나타내는 지 여부 만 함수에서 판단 할 수는 없습니다. 그것은 그들이 어떤 인수에 전달하는지에 달려 있습니다.

반면에 오버플로 안전 부호가있는 정수 연산의 간단한 예제를 고려하십시오.

int zero(int x) {
    return x - x;  /* Cannot overflow */
}

빼기 연산자의 피연산자 사이의 관계는 빼기가 절대 오버플로되지 않도록합니다. 또는 좀 더 실제적인 예를 생각해보십시오.

int sizeDelta(FILE *f1, FILE *f2) {
    int count1 = 0;
    int count2 = 0;
    while (fgetc(f1) != EOF) count1++;  /* might overflow */
    while (fgetc(f2) != EOF) count2++;  /* might overflow */

    return count1 - count2; /* provided no UB to this point, will not overflow */
}

카운터가 개별적으로 오버플로하지 않는 한 최종 빼기의 피연산자는 모두 음수가 아닙니다. 두 값 사이의 모든 차이는 int 로 나타낼 수 있습니다.

초기화되지 않은 변수 사용

int a; 
printf("%d", a);

변수 a 는 자동 저장 기간이있는 int 입니다. 예제 코드는 위의 초기화되지 않은 변수의 값 (인쇄하려고 a 초기화되지 않았다)를. 초기화되지 않은 자동 변수에는 불확정 값이 있습니다. 이들에 액세스하면 정의되지 않은 동작이 발생할 수 있습니다.

참고 : static 키워드가없는 전역 변수 를 포함하여 정적 또는 스레드 로컬 저장소가있는 변수 는 0 또는 초기화 된 값으로 초기화됩니다. 그러므로 다음은 합법적입니다.

static int b;
printf("%d", b);

가장 흔한 실수는 카운터 역할을하는 변수를 0으로 초기화하지 않는 것입니다. 값을 추가하지만 초기 값은 가비지이기 때문에 컴파일러가 터미널 경고를주는 질문에서와 같이 정의되지 않은 동작 을 호출 합니다. 이상한 상징 .

예:

#include <stdio.h>

int main(void) {
    int i, counter;
    for(i = 0; i < 10; ++i)
        counter += i;
    printf("%d\n", counter);
    return 0;
}

산출:

C02QT2UBFVH6-lm:~ gsamaras$ gcc main.c -Wall -o main
main.c:6:9: warning: variable 'counter' is uninitialized when used here [-Wuninitialized]
        counter += i;
        ^~~~~~~
main.c:4:19: note: initialize the variable 'counter' to silence this warning
    int i, counter;
                  ^
                   = 0
1 warning generated.
C02QT2UBFVH6-lm:~ gsamaras$ ./main
32812

위의 규칙은 포인터에도 적용됩니다. 예를 들어, 정의되지 않은 동작의 결과는 다음과 같습니다.

int main(void)
{
    int *p;
    p++; // Trying to increment an uninitialized pointer.
}

위의 코드는 자체적으로 오류 또는 분할 오류를 일으키지 않을 수도 있지만 나중에이 포인터를 역 참조하려고하면 정의되지 않은 동작이 발생합니다.

변수에 대한 포인터를 수명 초과로 간접 참조

int* foo(int bar)
{
    int baz = 6;
    baz += bar;
    return &baz; /* (&baz) copied to new memory location outside of foo. */
} /* (1) The lifetime of baz and bar end here as they have automatic storage   
   * duration (local variables), thus the returned pointer is not valid! */

int main (void)
{
    int* p;

    p = foo(5);  /* (2) this expression's behavior is undefined */
    *p = *p - 6; /* (3) Undefined behaviour here */

    return 0;
}

유용하게도 일부 컴파일러가이를 지적합니다. 예를 들어, gcc 는 다음과 같이 경고합니다.

warning: function returns address of local variable [-Wreturn-local-addr]

clang 은 다음과 같이 경고합니다.

warning: address of stack memory associated with local variable 'baz' returned 
[-Wreturn-stack-address]

위의 코드는 그러나 컴파일러는 복잡한 코드에서 도움을 줄 수 없을 수도 있습니다.

(1) static 으로 선언 된 변수에 대한 참조를 반환하는 것은 현재 범위를 벗어난 후에 변수가 소멸되지 않으므로 정의 된 동작입니다.

(2) ISO / IEC 9899 : 2011 6.2.4 §2에 따르면 "포인터가 가리키는 대상이 수명이 다할 때 포인터의 값은 불확정해진다."

(3) 함수 foo 의해 반환 된 포인터를 역 참조하는 것은 그것이 참조하는 메모리가 불확정 값을 가지기 때문에 정의되지 않은 동작입니다.

0으로 나누기

int x = 0;
int y = 5 / x;  /* integer division */

또는

double x = 0.0;
double y = 5.0 / x;  /* floating point division */

또는

int x = 0;
int y = 5 % x;  /* modulo operation */

두 번째 피연산자 (x)의 값이 0 인 각 예제의 두 번째 행에 대해 동작은 정의되지 않습니다.

부동 소수점 연산의 대부분의 구현표준 (예 : IEEE 754)을 따를 것이며, C 표준에 따라 연산이 정의되지 않았다고하더라도 0으로 나누기와 같은 연산은 일관된 결과 (예 : INFINITY )를 갖게됩니다.

할당 된 청크 너머의 메모리 액세스

n 요소를 포함하는 메모리 조각에 대한 포인터는 범위 memorymemory + (n - 1) 에있는 경우에만 참조 해제 될 수 있습니다. 해당 범위를 벗어나는 포인터를 역 참조하면 정의되지 않은 동작이 발생합니다. 예를 들어, 다음 코드를 고려하십시오.

int array[3];
int *beyond_array = array + 3;
*beyond_array = 0; /* Accesses memory that has not been allocated. */

세 번째 줄은 배열의 3 번째 요소 인 배열에서 네 번째 요소에 액세스하므로 정의되지 않은 동작이 발생합니다. 비슷하게, 다음 코드 부분에서 두 번째 행의 동작도 잘 정의되어 있지 않습니다.

int array[3];
array[3] = 0;

배열의 마지막 요소를 가리키는 것은 정의되지 않은 동작 ( beyond_array = array + 3 은 여기에서 잘 정의 됨)이 아니라 참조 해제 ( *beyond_array 는 정의되지 않은 동작)입니다. 이 규칙은 동적으로 할당 된 메모리 ( malloc 통해 생성 된 버퍼 등)에도 적용됩니다.

겹치는 메모리 복사하기

다양한 표준 라이브러리 함수는 하나의 메모리 영역에서 다른 메모리 영역으로 바이트 시퀀스를 복사하는 효과가 있습니다. 대부분의 함수는 소스와 대상 영역이 겹칠 때 정의되지 않은 동작을합니다.

예를 들어, 이건 ...

#include <string.h> /* for memcpy() */

char str[19] = "This is an example";
memcpy(str + 7, str, 10);

... 소스 및 대상 메모리 영역이 3 바이트 겹치는 10 바이트를 복사하려고 시도합니다. 시각화하려면 :

               overlapping area
               |
               _ _
              |   |
              v   v
T h i s   i s   a n   e x a m p l e \0
^             ^
|             |
|             destination
|
source

오버랩 때문에 결과 동작은 정의되지 않습니다.

이러한 종류의 제한이있는 표준 라이브러리 함수에는 memcpy() , strcpy() , strcat() , sprintf()sscanf() 있습니다. 표준에서는 다음과 같은 여러 기능에 대해 설명합니다.

겹치는 객체간에 복사가 발생하면 동작은 정의되지 않습니다.

memmove() 함수는이 규칙의 기본 예외입니다. 그 정의는 소스 데이터가 처음 임시 버퍼에 복사 된 다음 대상 주소에 기록 된 것처럼 함수가 작동하도록 지정합니다. 소스 영역과 대상 영역이 겹치거나 하나도 필요하지 않기 때문에 memmove() 는 이러한 경우 잘 정의 된 동작을합니다.

구별은 효율 효율성을 반영합니다. 일반성 교환. 이러한 함수와 같은 복사는 일반적으로 메모리의 분리 된 영역 사이에서 발생하며 종종 개발시 특정 인스턴스의 메모리 복사가 해당 범주에 있는지 여부를 알 수 있습니다. 비 중복이 있다고 가정하면 가정이 유지되지 않을 때 정확한 결과를 신뢰성있게 생성하지 않는 비교적 효율적인 구현이 제공됩니다. 대부분의 C 라이브러리 함수는보다 효율적인 구현이 허용되며, 소스와 대상이 겹칠 수도 중복 될 수도있는 경우를 처리하여 틈에 memmove() 가 채워집니다. 그러나 모든 경우에 올바른 효과를 내기 위해서는 추가 테스트를 수행하거나 비교적 효율적으로 구현하지 않아야합니다.

메모리에 의해 지원되지 않는 초기화되지 않은 객체 읽기

C11

객체를 읽으면 객체가 1 인 경우 정의되지 않은 동작이 발생합니다.

  • 초기화되지 않은
  • 자동 저장 기간으로 정의
  • 그 주소는 절대 받아 들여지지 않는다.

아래 예제의 변수 a는 모든 조건을 충족시킵니다.

void Function( void )
{
    int a;
    int b = a;
} 

1 (인용 부호 : ISO : IEC 9899 : 201X 6.3.2.1 Lvalues, 배열 및 함수 지정자 2)
lvalue가 레지스터 저장 클래스로 선언 된 자동 저장 기간 객체를 지정하고 (주소를 가져 본 적이없는 경우) 해당 객체는 초기화되지 않은 상태 (초기화되지 않은 객체로 선언되지 않고 사용 전에 수행되지 않은 객체 임) ), 동작은 정의되지 않습니다.

데이터 경쟁

C11

C11은 여러 개의 실행 스레드에 대한 지원을 도입하여 데이터 경합의 가능성을 제시했습니다. 그것의 목적은 접근 중 적어도 하나는 비 원자 두 개의 서로 다른 쓰레드에 의해 1 액세스하는 경우 프로그램 데이터 레이스 포함 적어도 하나의 개체를 수정하고, 프로그램의 의미는 확인되지 두 접근 할 수없는 오버랩 그 시간적으로. 2 관련된 액세스의 실제 동시성은 데이터 경쟁에 대한 조건이 아니라는 점에 유의하십시오. 데이터 경주는 서로 다른 스레드의 메모리 뷰에서 (허용 된) 불일치로 인해 발생하는 더 광범위한 클래스의 문제를 다룹니다.

다음 예제를 고려하십시오.

#include <threads.h>

int a = 0;

int Function( void* ignore )
{
    a = 1;

    return 0;
}

int main( void )
{
    thrd_t id;
    thrd_create( &id , Function , NULL );

    int b = a;

    thrd_join( id , NULL );
}

메인 스레드 호출 thrd_create 새 스레드 실행 기능을 시작하는 Function . 두 번째 스레드는 a 수정하고 주 스레드 a 읽습니다. 이러한 액세스는 모두 원자가 아니며 두 스레드는 겹치지 않도록 개별적으로 또는 공동으로 수행하지 않으므로 데이터 경쟁이 있습니다.

이 프로그램이 데이터 경쟁을 피할 수있는 방법 중에는

  • 주 스레드는 다른 스레드를 시작하기 전에 a 의 읽기를 수행 할 수 있습니다.
  • 주 스레드는 thrd_join 을 통해 다른 스레드가 종료되었음을 확인한 후에 a 의 읽기를 수행 할 수 있습니다.
  • 스레드가 뮤텍스를 통해 자신의 액세스를 동기화 할 수 액세스하기 전에 그 뮤텍스를 잠금 각각 a 하고 나중에 그것을 잠금 해제.

뮤텍스 옵션이 보여 주듯이, 데이터 경쟁을 피하는 것은 메인 쓰레드가 그것을 읽기 전에 아이 쓰레드를 수정 a 것과 같은 특정 연산 순서를 보장 할 필요가 없다. 주어진 실행에 대해 하나의 액세스가 다른 하나의 액세스보다 먼저 일어날 것임을 보장하기에 충분합니다 (데이터 경쟁을 피하기 위해).


1 오브젝트 수정 또는 읽기.

2 (ISO에서 인용 : IEC 9889 : 201x, 5.1.2.4 절. "멀티 스레드 실행 및 데이터 레이스")
프로그램의 실행에는 서로 다른 쓰레드에 충돌하는 두 개의 액션이 포함되어 있고 적어도 하나는 원 자성이 아니며 다른 액션은 둘 다 발생하지 않는다면 데이터 경쟁이 포함됩니다. 그러한 데이터 경쟁으로 인해 정의되지 않은 동작이 발생합니다.

해제 된 포인터의 값 읽기

심지어 해제 된 포인터의 값을 읽는 것만으로도 포인터를 참조 해제하지 않고도 정의되지 않은 동작 (UB)이 가능합니다.

char *p = malloc(5);
free(p);
if (p == NULL) /* NOTE: even without dereferencing, this may have UB */
{

}

ISO / IEC 9899 : 2011 , 섹션 6.2.4 §2 인용 :

[...] 포인터가 가리키는 객체가 수명이 끝나면 포인터의 값은 불확정 해집니다.

명백하게 해가없는 비교 나 산술을 포함하여 무엇이든 불확정 메모리를 사용하면 해당 값이 유형의 트랩 표현 일 수있는 경우 정의되지 않은 동작이 발생할 수 있습니다.

문자열 리터럴 수정

이 코드 예제에서 char 포인터 p 는 문자열 리터럴의 주소로 초기화됩니다. 문자열 리터럴을 수정하려고하면 정의되지 않은 동작이 발생합니다.

char *p = "hello world";
p[0] = 'H'; // Undefined behavior

그러나 char 의 변경 가능한 배열을 직접 또는 포인터를 통해 수정하는 것은 이니셜 라이저가 리터럴 문자열 인 경우에도 당연히 정의되지 않은 동작이 아닙니다. 다음은 정상입니다.

char a[] = "hello, world";
char *p = a;

a[0] = 'H';
p[7] = 'W';

그 이유는 배열이 초기화 될 때마다 문자열 리터럴이 효과적으로 배열에 복사되기 때문입니다 (정적 지속 기간이있는 변수에 대해 한 번, 자동 또는 스레드 지속 기간이있는 변수에 대해 배열이 생성 될 때마다 할당 된 기간의 변수는 초기화되지 않습니다). 배열 내용을 수정하는 것이 좋습니다.

메모리 두 번 해제

메모리를 두 번 비우는 것은 정의되지 않은 동작입니다.

int * x = malloc(sizeof(int));
*x = 9;
free(x);
free(x);

표준에서 인용 (7.20.3.2 C99의 자유로운 기능) :

그렇지 않으면 인수가 calloc, malloc 또는 realloc 함수에 의해 이전에 반환 된 포인터와 일치하지 않거나 free 또는 realloc에 ​​대한 호출로 공간 할당이 취소 된 경우 동작은 정의되지 않습니다.

printf에서 잘못된 형식 지정자 사용

printf 의 첫 번째 인수에서 잘못된 형식 지정자를 사용하면 정의되지 않은 동작이 호출됩니다. 예를 들어 아래 코드는 정의되지 않은 동작을 호출합니다.

long z = 'B';
printf("%c\n", z);

여기 또 다른 예가있다.

printf("%f\n",0);

위 코드 행은 정의되지 않은 동작입니다. %f 는 (는) double을 기대합니다. 그러나 0은 int 유형입니다.

컴파일러가 컴파일 할 때 적절한 플래그를 켜면 컴파일러는 대개 이러한 경우를 피할 수 있습니다 ( clanggcc -Wformat ). 마지막 예제에서 :

warning: format specifies type 'double' but the argument has type
      'int' [-Wformat]
    printf("%f\n",0);
            ~~    ^
            %d

포인터 형식 간의 변환이 잘못 정렬 된 결과를 생성 함

다음 잘못된 포인터 정렬로 인해 정의되지 않은 동작을 할 수 있습니다.

 char *memory_block = calloc(sizeof(uint32_t) + 1, 1);
 uint32_t *intptr = (uint32_t*)(memory_block + 1);  /* possible undefined behavior */
 uint32_t mvalue = *intptr;

정의되지 않은 동작은 포인터가 변환 될 때 발생합니다. C11에 따르면 두 포인터 유형 간의 변환이 잘못 정렬 된 결과를 생성하는 경우 (6.3.2.3) 동작은 정의되지 않습니다 . 여기서 uint32_t 는 예를 들어 2 또는 4의 정렬을 필요로 할 수 있습니다.

반면에 calloc 은 어떤 객체 형에 대해서도 적절하게 정렬 된 포인터를 리턴해야한다. 따라서 memory_block 은 초기 부분에 uint32_t 를 포함하도록 적절하게 정렬됩니다. 그런 다음 uint32_t 가 2 또는 4의 정렬을 필요로하는 시스템에서 memory_block + 1홀수 주소가되어 적절하게 정렬되지 않습니다.

C 표준은 이미 캐스트 연산이 정의되지 않았 음을 요구합니다. 주소가 세분화 된 플랫폼에서는 메모리 주소가 memory_block + 1 경우에도 정수 포인터로 표시되지 않을 수 있기 때문에 부과됩니다.

정렬 요구 사항에 상관없이 char * 를 다른 유형의 포인터로 캐스팅하면 파일 헤더 나 네트워크 패킷과 같은 압축 구조를 디코딩하는 데 잘못 사용되는 경우가 있습니다.

memcpy 를 사용하여 정렬되지 않은 포인터 변환으로 인해 발생하는 정의되지 않은 동작을 피할 수 있습니다.

memcpy(&mvalue, memory_block + 1, sizeof mvalue);

여기서 uint32_t* 로의 포인터 변환은 일어나지 않으며 바이트는 하나씩 복사됩니다.

이 예제에서는 다음과 mvalue 이유로 mvalue 유효한 값만 mvalue .

  • 우리는 calloc 사용했기 때문에 바이트가 제대로 초기화되었다. 우리의 경우 모든 바이트의 값은 0 이지만 다른 적절한 초기화가 수행됩니다.
  • uint32_t 는 정확한 너비 유형이며 패딩 비트가 없습니다.
  • 임의의 비트 패턴은 부호없는 유형의 유효한 표현입니다.

포인터가 올바르게 제한되지 않음

다음 코드에는 정의되지 않은 동작이 있습니다.

char buffer[6] = "hello";
char *ptr1 = buffer - 1;  /* undefined behavior */
char *ptr2 = buffer + 5;  /* OK, pointing to the '\0' inside the array */
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char *ptr4 = buffer + 7;  /* undefined behavior */

C11에 따르면 배열 객체와 정수 유형으로 또는 그 이상의 포인터를 더하거나 뺄 때 동일한 배열 객체를 가리 키지 않는 결과가 나오면 동작은 정의되지 않습니다 (6.5.6 ).

또한 배열을 넘어서는 포인터를 역 참조 하는 것은 자연적으로 정의되지 않은 동작입니다.

char buffer[6] = "hello";
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char value = *ptr3;       /* undefined behavior */

포인터를 사용하여 const 변수 수정하기

int main (void)
{
    const int foo_readonly = 10;
    int *foo_ptr;

    foo_ptr = (int *)&foo_readonly; /* (1) This casts away the const qualifier */
    *foo_ptr = 20; /* This is undefined behavior */

    return 0;
}

인용 ISO / IEC 9899 : 201x , 섹션 6.7.3 §2 :

const-qualified 형식이 아닌 lvalue를 사용하여 const 한정 형식으로 정의 된 개체를 수정하려고하면 동작이 정의되지 않습니다. [...]


(1) GCC에서 다음 경고를 throw 할 수 있습니다 : warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]

printf % s 변환에 대한 널 포인터 전달

printf%s 변환은 해당 인수 가 문자 유형 배열의 초기 요소에 대한 포인터임을 나타냅니다 . 널 포인터는 문자 유형 배열의 초기 요소를 가리 키지 않으므로 다음의 동작은 정의되지 않습니다.

char *foo = NULL;
printf("%s", foo); /* undefined behavior */

그러나 정의되지 않은 동작이 항상 프로그램 충돌을 의미하는 것은 아닙니다. 일부 시스템에서는 널 포인터가 역 참조 될 때 일반적으로 발생하는 크래시를 피하기위한 조치를 취합니다. 예를 들어, Glibc가 인쇄하는 것으로 알려져 있습니다.

(null)

위의 코드는 그러나 형식 문자열에 줄 바꾸기를 추가하면 충돌이 발생합니다.

char *foo = 0;
printf("%s\n", foo); /* undefined behavior */

이 경우 GCC는 printf("%s\n", argument); 하는 최적화 기능을 가지고 있기 때문에 발생합니다 printf("%s\n", argument); 의 호출에 putsputs(argument)puts Glibc의에서 널 포인터를 처리하지 않습니다. 이 모든 행동은 표준을 준수합니다.

null 포인터빈 문자열 과 다릅니다. 따라서 다음은 유효하며 정의되지 않은 동작이 없습니다. 그것은 단지 줄 바꿈을 출력합니다 :

char *foo = "";
printf("%s\n", foo);

식별자의 일관성없는 연결

extern int var;
static int var; /* Undefined behaviour */

C11, §6.2.2, 7 은 말한다 :

번역 단위 내에서 내부 식별자와 외부 식별자가 동일한 식별자가 나타나는 경우 동작이 정의되지 않습니다.

식별자의 이전 선언이 표시되면 이전 선언의 연결을 갖게됩니다. C11, §6.2.2, 4 는 그것을 허락한다 :

이전에 해당 선언자가 선언 한 범위 내에서 저장 클래스 지정자로 선언 된 식별자의 경우 31) 이전 선언이 내부 또는 외부 연결을 지정하는 경우 나중에 선언 할 때 식별자의 연결은 다음과 같습니다. 이전의 선언에서 명시된 연계. 이전 선언이 표시되지 않거나 이전 선언이 연결을 지정하지 않은 경우 식별자는 외부 연결을가집니다.

/* 1. This is NOT undefined */
static int var;
extern int var; 


/* 2. This is NOT undefined */
static int var;
static int var; 

/* 3. This is NOT undefined */
extern int var;
extern int var; 

입력 스트림에서 fflush 사용

POSIX 및 C 표준은 입력 스트림에서 fflush 를 사용하면 정의되지 않은 동작임을 명시 적으로 설명합니다. fflush 는 출력 스트림에 대해서만 정의됩니다.

#include <stdio.h>

int main()
{
    int i;
    char input[4096];

    scanf("%i", &i);
    fflush(stdin); // <-- undefined behavior
    gets(input);

    return 0;
}

입력 스트림에서 읽지 않은 문자를 버리는 표준 방법은 없습니다. 한편, 일부 구현에서는 stdin 버퍼를 지우기 위해 fflush 를 사용합니다. Microsoft는 입력 스트림에서 fflush 의 동작을 정의합니다. 스트림이 입력 용으로 열려 있으면 fflush 가 버퍼의 내용을 지 웁니다. POSIX.1-2008에 따르면 fflush 의 동작은 입력 파일을 찾지 못하면 정의되지 않습니다.

자세한 내용은 fflush(stdin) 사용을 참조하십시오.

음수 또는 형식의 너비를 사용하는 비트 시프트

시프트 수 값이 음수 이면 왼쪽 시프트오른쪽 시프트 가 모두 정의되지 않음 1 :

int x = 5 << -3; /* undefined */
int x = 5 >> -3; /* undefined */

왼쪽 시프트음수 값으로 수행되면 정의되지 않습니다.

int x = -5 << 3; /* undefined */

왼쪽 시프트양수 값으로 수행되고 수학적 값의 결과가 유형에서 표현할 수 없는 경우 정의되지 않음 1 :

/* Assuming an int is 32-bits wide, the value '5 * 2^72' doesn't fit 
 * in an int. So, this is undefined. */
       
int x = 5 << 72;

음의 값에 해당 우측 시프트 주 (.eg을 -5 >> 3 ) 미정이지만 구현 정의 아니다.


1 인용 ISO / IEC 9899 : 201x , 섹션 6.5.7 :

오른쪽 피연산자의 값이 음수이거나 승격 된 왼쪽 피연산자의 너비보다 크거나 같으면 동작이 정의되지 않습니다.

getenv, strerror 및 setlocale 함수에 의해 반환 된 문자열 수정

표준 함수 getenv() , strerror()setlocale() 의해 반환 된 문자열을 수정하면 정의되지 않습니다. 구현시 이러한 문자열에 정적 저장소를 사용할 수 있습니다.

getenv () 함수, C11, §7.22.4.7, 4 는 다음과 같이 말합니다.

getenv 함수는 일치하는 목록 구성원과 연관된 문자열에 대한 포인터를 반환합니다. 가리키는 문자열은 프로그램에 의해 수정되지 않고 getenv 함수에 대한 후속 호출에 의해 덮어 쓸 수 있습니다.

strerror () 함수, C11, §7.23.6.3, 4 는 다음과 같이 말합니다.

strerror 함수는 문자열에 대한 포인터를 반환하며, 그 내용은 지역에 따라 다릅니다. 가리키는 배열은 프로그램에 의해 수정되지 않지만 strerror 함수에 대한 후속 호출에 의해 덮어 쓸 수 있습니다.

setlocale () 함수, C11, §7.11.1.1, 8 은 다음과 같이 말합니다 :

setlocale 함수에 의해 반환 된 문자열에 대한 포인터는 해당 문자열 값과 관련된 범주를 가진 후속 호출이 프로그램의 로켈 부분을 복원하도록합니다. 가리키는 문자열은 프로그램에 의해 수정되지 않아야하지만, 이후에 setlocale 함수를 호출하여 덮어 쓸 수 있습니다.

마찬가지로 localeconv() 함수는 수정할 수없는 struct lconv 대한 포인터를 반환합니다.

localeconv () 함수, C11, §7.11.2.1, 8 은 다음과 같이 말합니다.

localeconv 함수는 채워진 객체에 대한 포인터를 반환합니다. 반환 값이 가리키는 구조는 프로그램에 의해 수정되지 않지만 이후의 localeconv 함수 호출로 덮어 쓸 수 있습니다.

`_Noreturn` 또는`noreturn` 함수 지정자로 선언 된 함수로부터 리턴하기

C11

함수 지정자 _Noreturn 은 C11에서 소개되었습니다. 헤더 <stdnoreturn.h> 매크로 제공 noreturn 로 확장 _Noreturn . 그래서 <stdnoreturn.h> _Noreturn 또는 noreturn 을 사용하면 _Noreturn .

_Noreturn (또는 noreturn )으로 선언 된 함수는 호출자에게 반환 할 수 없습니다. 이러한 함수 호출자에게 반환되면 동작은 정의되지 않습니다.

다음 예제에서 func()noreturn 지정자로 선언되지만 호출자에게 반환됩니다.

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void func(void);

void func(void)
{
    printf("In func()...\n");
} /* Undefined behavior as func() returns */

int main(void)
{
    func();
    return 0;
}

gccclang 은 위의 프로그램에 대한 경고를 생성합니다.

$ gcc test.c
test.c: In function ‘func’:
test.c:9:1: warning: ‘noreturn’ function does return
 }
 ^
$ clang test.c
test.c:9:1: warning: function declared 'noreturn' should not return [-Winvalid-noreturn]
}
^

잘 정의 된 동작을 가진 noreturn 을 사용한 예 :

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void my_exit(void);

/* calls exit() and doesn't return to its caller. */
void my_exit(void)
{
    printf("Exiting...\n");
    exit(0);
}

int main(void)
{
    my_exit();
    return 0;
}


Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow