[c] (왜) 초기화되지 않은 변수 정의되지 않은 동작을 사용하고 있습니까?

만약 내가 가지고 있다면:

unsigned int x;
x -= x;

이 표현식 뒤에는 0이 x 되어야한다는 것은 분명 하지만, 내가 보는 모든 곳에서 그들은 이 코드 의 동작x(빼기 전까지) 값이 아니라 정의되지 않았다고 말합니다 .

두 가지 질문 :

  • 이 코드 의 동작 이 실제로 정의되지 않았습니까?
    (예 : 호환 시스템에서 코드가 충돌하거나 더 나빠질 수 있습니까?)

  • 그렇다면 C 는 여기서 0 이 되어야한다는 것이 완벽하게 분명 할 때 동작 이 정의되지 않았다고 말하는 이유 는 무엇입니까?x

    즉 여기에서 행동을 정의하지 않으면 어떤 이점이 있습니까?

분명히, 컴파일러는 간단하게 사용할 수있는 어떤 쓰레기 값이 변수 안에 “편리한”것으로 간주하고, 같이 일하는 것이 목적 … 무슨 일이 그 방법 잘못?



답변

예,이 동작은 정의되지 않았지만 대부분의 사람들이 알고있는 것과는 다른 이유 때문입니다.

첫째, 단일화 된 값을 사용하는 것은 그 자체로 정의되지 않은 동작이 아니지만 값은 단순히 불확실합니다. 값이 유형에 대한 트랩 표현 인 경우 여기에 액세스하는 것은 UB입니다. 서명되지 않은 유형에는 트랩 표현이 거의 없으므로 그 쪽에서 상대적으로 안전합니다.

비헤이비어를 정의하지 않게 만드는 것은 변수의 추가 속성입니다. 즉, register주소를 사용하지 않는 ” 선언 할 수있는”변수 입니다. 이러한 변수는 “초기화되지 않은”일종의 추가 상태가 있고 유형 도메인의 값에 해당하지 않는 실제 CPU 레지스터가있는 아키텍처가 있기 때문에 특별히 처리됩니다.

편집 : 표준의 관련 문구는 6.3.2.1p2입니다.

lvalue가 레지스터 스토리지 클래스로 선언 될 수있는 자동 저장 기간의 객체를 지정하고 (그 주소를 사용하지 않은 경우) 해당 객체가 초기화되지 않은 경우 (이니셜 라이저로 선언되지 않고 사용 전에 할당되지 않은 경우) ), 동작이 정의되지 않았습니다.

명확하게하기 위해 다음 코드 모든 상황에서 합법적입니다.

unsigned char a, b;
memcpy(&a, &b, 1);
a -= a;
  • 여기서 a및 의 주소를 b가져 오므로 값은 불확실합니다.
  • unsigned char불확실한 값이 지정되지 않은 트랩 표현이 없기 때문에의 모든 값 unsigned char이 발생할 수 있습니다.
  • 끝에는 값이 a 있어야합니다0 .

EDIT2 : ab지정되지 않은 값이 :

3.19.3
이 국제 표준이 어떤 경우에도 값이 선택되는 요건을 부과하지 않는 관련 유형의 불특정 값
유효 값


답변

C 표준은 컴파일러에게 최적화를 수행 할 수있는 많은 권한을 제공합니다. 초기화되지 않은 메모리가 임의의 비트 패턴으로 설정되고 모든 작업이 작성된 순서대로 수행되는 프로그램의 순진한 모델을 가정하면 이러한 최적화의 결과는 놀라 울 수 있습니다.

참고 : 다음 예제는 x주소를 사용하지 않았기 때문에 유효하므로 “등록과 유사”합니다. 유형에 x트랩 표현이있는 경우에도 유효합니다 . 서명되지 않은 유형의 경우는 드물며 (최소한 1 비트의 스토리지를 “낭비”해야하며 문서화해야 함) unsigned char. x서명 된 유형이있는 경우 구현은-(2 n-1 -1)과 2 n-1 -1 사이의 숫자가 아닌 비트 패턴을 트랩 표현으로 정의 할 수 있습니다. Jens Gustedt의 답변을 참조하십시오 .

레지스터가 메모리보다 빠르기 때문에 컴파일러는 레지스터를 변수에 할당하려고합니다. 프로그램은 프로세서에있는 레지스터보다 더 많은 변수를 사용할 수 있기 때문에 컴파일러는 레지스터 할당을 수행하여 다른 시간에 동일한 레지스터를 사용하는 다른 변수를 만듭니다. 프로그램 조각을 고려하십시오

unsigned x, y, z;   /* 0 */
y = 0;              /* 1 */
z = 4;              /* 2 */
x = - x;            /* 3 */
y = y + z;          /* 4 */
x = y + 1;          /* 5 */

라인 3이 평가 될 때 x아직 초기화되지 않았으므로 (컴파일러의 이유) 라인 3은 컴파일러가 알아낼만큼 똑똑하지 않은 다른 조건으로 인해 발생할 수없는 일종의 우연이어야합니다. z4 번 줄 이후 에는 사용 x되지 않고 5 번 줄 이전에는 사용되지 않으므로 두 변수에 동일한 레지스터를 사용할 수 있습니다. 따라서이 작은 프로그램은 레지스터에 대한 다음 작업으로 컴파일됩니다.

r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;

의 최종 값 x은의 최종 값 r0이고의 최종 값 y은의 최종 값입니다 r1. 이러한 값은 x = -3 및 y = -4이며 x제대로 초기화 된 경우 발생하는 5와 4가 아닙니다 .

보다 정교한 예를 보려면 다음 코드 조각을 고려하십시오.

unsigned i, x;
for (i = 0; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

컴파일러 condition가 부작용이 없음을 감지했다고 가정합니다 . condition수정하지 않기 때문에 x컴파일러는 루프를 통한 첫 번째 실행 x이 아직 초기화되지 않았으므로 액세스 할 수 없음을 알고 있습니다 . 따라서 루프 본문의 첫 번째 실행은와 동일 x = some_value()하므로 조건을 테스트 할 필요가 없습니다. 컴파일러는 여러분이 작성한 것처럼이 코드를 컴파일 할 수 있습니다.

unsigned i, x;
i = 0; /* if some_value() uses i */
x = some_value();
for (i = 1; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

이 컴파일러의 내부 모델링 될 수있는 방법에 따라 값이 있음을 고려하는 것입니다 x편리하다 어떤 값 만큼으로 x초기화되지 않습니다. 초기화되지 않은 변수가 정의되지 않은 경우의 동작은 단순히 지정되지 않은 값을 갖는 변수가 아니라 변수이기 때문에 컴파일러는 편리한 값 간의 특별한 수학적 관계를 추적 할 필요가 없습니다. 따라서 컴파일러는 위의 코드를 다음과 같이 분석 할 수 있습니다.

  • 첫 번째 루프 반복 중에는 x시간 -x이 평가 될 때까지 초기화되지 않습니다.
  • -x 정의되지 않은 동작이 있으므로 그 값은 무엇이든 편리합니다.
  • 최적화 규칙이 적용 되므로이 코드를 .condition ? value : valuecondition; value

질문의 코드와 마주 쳤을 때이 동일한 컴파일러 x = - x는 평가 될 때 의 값 -x이 무엇이든 편리 하다는 것을 분석 합니다. 따라서 할당을 최적화 할 수 있습니다.

위에서 설명한대로 동작하는 컴파일러의 예를 찾지는 않았지만 좋은 컴파일러가 시도하는 일종의 최적화입니다. 나는 하나를 만나도 놀라지 않을 것입니다. 다음은 프로그램이 충돌하는 컴파일러의 덜 그럴듯한 예입니다. (어떤 종류의 고급 디버깅 모드에서 프로그램을 컴파일하는 것은 그다지 믿을 수없는 일이 아닙니다.)

이 가상 컴파일러는 다른 메모리 페이지의 모든 변수를 매핑하고 페이지 속성을 설정하여 초기화되지 않은 변수에서 읽는 것이 디버거를 호출하는 프로세서 트랩을 유발하도록합니다. 변수에 대한 모든 할당은 먼저 해당 메모리 페이지가 정상적으로 매핑되었는지 확인합니다. 이 컴파일러는 고급 최적화를 수행하지 않습니다. 디버깅 모드에 있으며 초기화되지 않은 변수와 같은 버그를 쉽게 찾을 수 있습니다. 시 x = - x평가되고, 우측 트랩 발생 디버거 위로 발사.


답변

예, 프로그램이 충돌 할 수 있습니다. 예를 들어, CPU 인터럽트를 유발할 수있는 트랩 표현 (처리 할 수없는 특정 비트 패턴)이있을 수 있으며, 처리되지 않으면 프로그램이 중단 될 수 있습니다.

(최근 C11 초안의 6.2.6.1에 따르면) 특정 개체 표현은 개체 유형의 값을 나타낼 필요가 없습니다. 객체의 저장된 값에 이러한 표현이 있고 문자 유형이없는 lvalue 표현식에서 읽는 경우 동작이 정의되지 않습니다. 이러한 표현이 문자 유형이없는 lvalue 표현식에 의해 객체의 전체 또는 일부를 수정하는 부작용에 의해 생성되는 경우 동작은 정의되지 않습니다 .50) 이러한 표현을 트랩 표현이라고합니다.

(이 설명 unsigned int은 실제 시스템에서는 드문 트랩 표현을 가질 수있는 플랫폼에만 적용됩니다 . 표준의 현재 표현으로 이어지는 대체 및 아마도 더 일반적인 원인에 대한 자세한 내용과 참조는 주석을 참조하십시오.)


답변

(이 답변은 C 1999를 다룹니다. C 2011의 경우 Jens Gustedt의 답변을 참조하십시오.)

C 표준은 초기화되지 않은 자동 저장 기간의 객체 값을 사용하는 것이 정의되지 않은 동작이라고 말하지 않습니다. C 1999 표준은 6.7.8 10에서 “자동 저장 기간이있는 객체가 명시 적으로 초기화되지 않으면 그 값은 불확실합니다”라고 말합니다. (이 단락은 정적 객체가 초기화되는 방법을 정의하므로 우리가 염려하는 유일한 초기화되지 않은 객체는 자동 객체입니다.)

3.17.2는 “미정 값”을 “미지정 값 또는 트랩 표현”으로 정의합니다. 3.17.3은 “미지정 값”을 “이 국제 표준이 어떤 경우에도 값이 선택되는 요건을 부과하지 않는 관련 유형의 유효한 값”으로 정의합니다.

따라서 초기화되지 unsigned int x않은 값이 지정 되지 않은 경우 x -= x0을 생성해야합니다. 그것은 함정 표현인지에 대한 의문을 남깁니다. 트랩 값에 액세스하면 6.2.6.1 5에 따라 정의되지 않은 동작이 발생합니다.

부동 소수점 숫자의 신호 NaN과 같은 일부 유형의 객체에는 트랩 표현이있을 수 있습니다. 그러나 부호없는 정수는 특별합니다. 6.2.6.2에 따라 unsigned int의 N 값 비트는 각각 2의 거듭 제곱을 나타내고 값 비트의 각 조합은 0에서 2 N -1 사이의 값 중 하나를 나타냅니다 . 따라서 부호없는 정수는 패딩 비트의 일부 값 (예 : 패리티 비트)으로 인해 트랩 표현 만 가질 수 있습니다.

대상 플랫폼에서 unsigned int에 패딩 비트가없는 경우 초기화되지 않은 unsigned int는 트랩 표현을 가질 수 없으며 해당 값을 사용하면 정의되지 않은 동작이 발생할 수 없습니다.


답변

예, 정의되지 않았습니다. 코드가 충돌 할 수 있습니다. C는 일반 규칙에 예외를 적용 할 특별한 이유가 없기 때문에 동작이 정의되지 않았다고 말합니다. 장점은 정의되지 않은 동작의 다른 모든 경우와 동일한 장점입니다. 컴파일러는이 작업을 수행하기 위해 특수 코드를 출력 할 필요가 없습니다.

분명히, 컴파일러는 변수 내에서 “편리한”것으로 간주되는 가비지 값을 간단히 사용할 수 있으며 의도 한대로 작동합니다 … 그 접근 방식에 어떤 문제가 있습니까?

왜 그런 일이 일어나지 않는다고 생각하십니까? 그것이 정확히 취해진 접근 방식입니다. 컴파일러가 작동하도록 할 필요는 없지만 실패하도록 만들 필요는 없습니다.


답변

초기화되지 않았거나 다른 이유로 인해 불확실한 값을 보유하는 모든 유형의 변수에 대해 해당 값을 읽는 코드에 다음이 적용됩니다.

  • 변수가 자동 저장 기간을 가지고 경우 그 주소가 촬영이없는 코드는 항상 정의되지 않은 동작 [1]를 호출합니다.
  • 그렇지 않으면 시스템이 주어진 변수 유형에 대한 트랩 표현을 지원하는 경우 코드는 항상 정의되지 않은 동작을 호출합니다 [2].
  • 그렇지 않고 트랩 표현이없는 경우 변수는 지정되지 않은 값을 사용합니다. 이 지정되지 않은 값이 변수를 읽을 때마다 일관성이 있다는 보장은 없습니다. 그러나 트랩 표현이 아님을 보장하므로 정의되지 않은 동작을 호출하지 않도록 보장됩니다 [3].

    이 값은 프로그램 충돌을 일으키지 않고 안전하게 사용할 수 있지만, 이러한 코드는 트랩 표현이있는 시스템으로 이식 할 수 없습니다.


[1] : C11 6.3.2.1 :

lvalue가 레지스터 스토리지 클래스로 선언 될 수있는 자동 저장 기간의 객체를 지정하고 (그 주소를 사용하지 않은 경우) 해당 객체가 초기화되지 않은 경우 (이니셜 라이저로 선언되지 않고 사용 전에 할당되지 않은 경우) ), 동작이 정의되지 않았습니다.

[2] : C11 6.2.6.1 :

특정 객체 표현은 객체 유형의 값을 나타낼 필요가 없습니다. 객체의 저장된 값에 이러한 표현이 있고 문자 유형이없는 lvalue 표현식에서 읽는 경우 동작이 정의되지 않습니다. 이러한 표현이 문자 유형이없는 lvalue 표현에 의해 객체의 전체 또는 일부를 수정하는 부작용에 의해 생성되는 경우 동작은 정의되지 않습니다 .50) 이러한 표현을 트랩 표현이라고합니다.

[3] C11 :

3.19.2
불확정 값
불특정 값 또는 트랩 표현 중

3.19.3
불특정 값
유효 값이 국제 표준은 어떤 경우에도 값이 선택되는 요건을 부과하지 않는 관련 유형의 유효 값
비고 불특정 값은 트랩 표현이 될 수 없습니다.

3.19.4
트랩 표현
객체 유형의 값을 나타낼 필요가없는 객체 표현


답변

많은 답변이 초기화되지 않은 레지스터 액세스를 트랩하는 프로세서에 초점을 맞추고 있지만, UB를 악용하려는 특별한 노력을하지 않는 컴파일러를 사용하여 이러한 트랩이없는 플랫폼에서도 기발한 동작이 발생할 수 있습니다. 코드를 고려하십시오.

volatile uint32_t a,b;
uin16_t moo(uint32_t x, uint16_t y, uint32_t z)
{
  uint16_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z;
  return temp;
}

로드 및 저장 이외의 모든 명령어가 32 비트 레지스터에서 작동하는 ARM과 같은 플랫폼 용 컴파일러는 다음과 같은 방식으로 코드를 합리적으로 처리 할 수 ​​있습니다.

volatile uint32_t a,b;
// Note: y is known to be 0..65535
// x, y, and z are received in 32-bit registers r0, r1, r2
uin32_t moo(uint32_t x, uint32_t y, uint32_t z)
{
  // Since x is never used past this point, and since the return value
  // will need to be in r0, a compiler could map temp to r0
  uint32_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z & 0xFFFF;
  return temp;
}

휘발성 읽기 중 하나가 0이 아닌 값을 생성하면 r0은 0 … 65535 범위의 값으로로드됩니다. 그렇지 않으면 0..65535 범위의 값이 아닐 수있는 함수가 호출 될 때 보유한 모든 값 (즉, x에 전달 된 값)을 생성합니다. 표준에는 유형이 uint16_t이지만 값이 0..65535 범위를 벗어난 값의 동작을 설명하는 용어가 없습니다. 단, 그러한 동작을 생성 할 수있는 모든 동작이 UB를 호출한다는 점이 다릅니다.