C ++ 표준은 초기화되지 않은 bool이 프로그램을 중단시킬 수 있습니까?


500

C ++ 의 "정의되지 않은 동작" 은 컴파일러가 원하는 모든 작업을 수행 할 수 있다는 것을 알고 있습니다. 그러나 코드가 충분히 안전하다고 가정하면서 충돌이 발생했습니다.

이 경우 실제 문제는 특정 컴파일러를 사용하는 특정 플랫폼에서만 최적화가 활성화 된 경우에만 발생했습니다.

문제를 재현하고 최대한 단순화하기 위해 여러 가지를 시도했습니다. 여기라는 함수의 추출물의 Serialize부울 매개 변수를 사용하고 문자열을 복사 할, true또는 false기존 대상 버퍼가.

이 함수가 코드 검토에 포함되어 있습니까? 실제로 bool 매개 변수가 초기화되지 않은 값인 경우 충돌 할 수 있음을 알 수있는 방법이 없습니까?

// Zero-filled global buffer of 16 characters
char destBuffer[16];

void Serialize(bool boolValue) {
    // Determine which string to print based on boolValue
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    const size_t len = strlen(whichString);

    // Copy string into destination buffer, which is zero-filled (thus already null-terminated)
    memcpy(destBuffer, whichString, len);
}

이 코드가 clang 5.0.0 + 최적화로 실행되면 충돌이 발생합니다.

예상되는 삼항 연산자 boolValue ? "true" : "false"는 저에게 충분히 안전 해 보였습니다. "쓰레기 값이 무엇이든 boolValue상관없이 그것이 참이나 거짓으로 평가되기 때문에 중요하지 않습니다."

분해의 문제를 보여주는 컴파일러 탐색기 예제 를 설정했습니다 . 여기에서 완전한 예제입니다. 참고 : 문제를 재현하기 위해 Clang 5.0.0을 -O2 최적화와 함께 사용했습니다.

#include <iostream>
#include <cstring>

// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
    bool uninitializedBool;

   __attribute__ ((noinline))  // Note: the constructor must be declared noinline to trigger the problem
   FStruct() {};
};

char destBuffer[16];

// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
    // Determine which string to print depending if 'boolValue' is evaluated as true or false
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    size_t len = strlen(whichString);

    memcpy(destBuffer, whichString, len);
}

int main()
{
    // Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
    FStruct structInstance;

    // Output "true" or "false" to stdout
    Serialize(structInstance.uninitializedBool);
    return 0;
}

옵티 마이저 때문에 문제가 발생합니다. 문자열 "true"와 "false"의 길이가 1만큼만 다르다는 것을 유추 할 수있을만큼 영리했습니다. 따라서 실제로 길이를 계산하는 대신 bool 자체의 값을 사용해야합니다. 기술적으로 0 또는 1이며 다음과 같습니다.

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

이것은 "영리한"것이지만, 내 질문은 : C ++ 표준은 컴파일러가 bool이 '0'또는 '1'의 내부 숫자 표현 만 가질 수 있다고 가정하고 그런 식으로 사용할 수 있습니까?

또는 이것은 구현 정의의 경우입니까?이 경우 구현은 모든 부울에 0 또는 1 만 포함하고 다른 값은 정의되지 않은 동작 영역이라고 가정합니다.


200
좋은 질문입니다. 그것은 정의되지 않은 행동이 단지 이론적 인 관심사가 아닌 방법을 확실하게 보여줍니다. 사람들이 UB의 결과로 어떤 일이 일어날 수 있다고 말할 때, 그 "모든 것"은 정말 놀랍습니다. 정의되지 않은 동작이 여전히 예측 가능한 방식으로 나타난다 고 가정 할 수도 있지만, 요즘에는 전혀 최적화되지 않은 최신 옵티마이 저가 있습니다. OP는 MCVE를 만드는 데 시간이 걸리고 문제를 철저히 조사하고 분해를 검사 한 후 명확하고 간단한 질문을했습니다. 더 요청할 수 없습니다.
John Kugelman 2019 년

7
"0이 아닌 값으로 평가"의 요구 사항은 " true부울에 할당"( static_cast<bool>()특정에 따라 암시 적으로 호출 할 수 있음)을 포함하는 부울 연산에 대한 규칙 입니다. 그러나 bool컴파일러 가 선택한 내부 표현에 대한 요구 사항은 아닙니다 .
Euro Micelli 2019 년

2
의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
Samuel Liew

3
이와 관련하여 이진 비 호환성의 "재미있는"소스입니다. 함수를 호출하기 전에 값을 0으로 채우는 ABI A가 있지만 매개 변수가 0으로 채워진 것으로 가정하고 반대의 ABI B (제로 패드는 아니지만 0으로 가정하지 않도록)를 컴파일하는 경우 패딩 매개 변수)를 사용하면 대부분 작동하지만 B ABI를 사용하는 함수는 '작은'매개 변수를 사용하는 A ABI를 사용하여 함수를 호출하면 문제가 발생합니다. IIRC는 xlang에서 clang 및 ICC와 함께 제공됩니다.
TLW

1
@TLW : 표준은 구현이 외부 코드에 의해 호출되거나 호출되는 수단을 제공 할 것을 요구하지는 않지만, 관련이있는 구현을 위해 그러한 것들을 지정하는 수단을 갖는 것이 도움이되었을 것입니다 (세부 사항이 아닌 구현) 해당 속성은 무시할 수 있습니다).
supercat

답변:


285

예, ISO C ++은 이러한 선택을 구현하기 위해 구현을 허용하지만 필수는 아닙니다.

또한 ISO C ++을 사용하면 프로그램에서 UB를 발견 한 경우 (예 : 오류를 찾는 데 도움이 됨) 의도적으로 충돌하는 코드 (예 : 잘못된 명령으로)를 생성 할 수 있습니다. (또는 DeathStation 9000이기 때문에. C ++ 구현이 실제 목적에 유용하기에는 엄격하게 준수하는 것만으로는 충분하지 않습니다). 따라서 ISO C ++을 사용하면 컴파일러는 초기화되지 않은을 읽는 비슷한 코드에서도 (다른 이유로) 충돌 한 asm을 만들 수 uint32_t있습니다. 비록 트랩 표현이없는 고정 레이아웃 유형이어야합니다.

실제 구현이 어떻게 작동하는지에 대한 흥미로운 질문이지만 대답이 다르더라도 현대 C ++은 이식 가능한 버전의 어셈블리 언어가 아니기 때문에 코드가 여전히 안전하지 않다는 것을 기억하십시오.


x86-64 System V ABI를 컴파일하고 있는데 bool, 레지스터의 함수 arg가 비트 패턴 false=0true=1 레지스터 1 의 하위 8 비트로 표시되도록 지정합니다 . 메모리에서 bool1 바이트 유형은 다시 0 또는 1의 정수 값을 가져야합니다.

(ABI는 동일한 플랫폼의 컴파일러가 동의하는 일련의 구현 선택 사항이므로 유형 크기, 구조체 레이아웃 규칙 및 호출 규칙을 포함하여 서로의 기능을 호출하는 코드를 만들 수 있습니다.)

ISO C ++에서는이를 지정하지 않지만이 ABI 결정은 bool-> int 변환을 저렴하게 (단지 확장없이)하기 때문에 널리 퍼져 있습니다. 컴파일러가 boolx86이 아닌 모든 아키텍처에 대해 0 또는 1을 가정하지 못하게하는 ABI는 알지 못합니다. 이것은 최적화 등 허용 !mybool으로는 xor eax,1: 낮은 비트 플립 단일 CPU 인스트럭션에 0과 1 사이의 비트 / 정수 / BOOL 플립 수있는 모든 가능한 코드 . 또는 유형 a&&b에 대한 비트 AND로 컴파일 합니다 bool. 일부 컴파일러는 실제로 컴파일러에서 부울 값을 8 비트로 사용합니다. 그들에 대한 작업이 비효율적입니까? .

일반적으로 as-if 규칙을 사용하면 컴파일러에서 컴파일 할 대상 플랫폼에서 사실 활용할 수 있습니다. 최종 결과는 C ++ 소스와 동일한 외부에서 볼 수있는 동작을 구현하는 실행 가능한 코드이기 때문입니다. (정의되지 않은 동작이 실제로 "외부 적으로 볼 수있는"항목에 적용되는 모든 제한 사항 : 디버거가 아니라 올바른 형식의 / 합법적 인 C ++ 프로그램의 다른 스레드에서 발생합니다.)

컴파일러는 코드 생성에서 ABI 보증을 최대한 활용하고 찾은 것과 같은 코드를 최적화 strlen(whichString)합니다
5U - boolValue.
(BTW,이 최적화는 일종의 영리하지만 memcpy즉각적인 데이터 저장소로 분기 및 인라인에 비해 근시안적 일 수 있습니다.)

또는 컴파일러가 포인터 테이블을 생성하고 bool다시 0 또는 1이라고 가정 하고 정수 값으로 색인을 생성 할 수 있습니다 ( 이 가능성은 @Barmar의 답변이 제안한 것 입니다).


귀하의 __attribute((noinline))최적화를 생성자로 사용할 스택에서 바이트를로드 단지 그 소리에지도 활성화 uninitializedBool. 또한 상기 목적을위한 공간을 만들어 main으로 push rax(효율적인로서 대해 작고 다양한 이유로있는 sub rsp, 8어떤 정도로 쓰레기가 항목을 AL에 있었다) main가 사용되는 값이다 uninitializedBool. 이것이 실제로 당신이 아닌 값을 얻은 이유 0입니다.

5U - random garbage부호없는 큰 값으로 쉽게 줄 바꿈하여 memcpy가 매핑되지 않은 메모리로 이동할 수 있습니다. 대상이 스택이 아닌 정적 저장소에 있으므로 반환 주소 나 무언가를 덮어 쓰지 않습니다.


다른 구현은 다른 선택을 할 수 있습니다 (예 : false=0및) true=any non-zero value. 그리고 아마 그 소리하는 코드를하지 것이라고에 대한 충돌 UB의 특정 인스턴스입니다. (하지만 원하는 경우 여전히 허용됩니다.) x86-64가 수행하는 다른 작업을 선택하는 구현에 대해서는 알지 bool못하지만 C ++ 표준은 아무도하지 않거나 원하지 않는 많은 일을 허용합니다. 현재 CPU와 같은 하드웨어.

ISO C ++에서는의 객체 표현을 검사하거나 수정할 때 찾을 수있는 내용을 지정하지 않은 상태로 둡니다bool . (예를 들어 ,에 별명을 지정할 수 memcpy있으므로 boolinto unsigned char를 사용하면 패딩 비트가 보장되지 않으므로 C ++ 표준은 공식적으로 UB없이 객체 표현을 16 진수 덤프 할 수 있습니다. 객체를 복사하기위한 포인터 캐스팅 표현은 물론 을 할당하는 것과 다르 므로 부울 화를 0 또는 1로 지정하지 않으면 원시 객체 표현을 얻을 수 있습니다.)char*unsigned charchar foo = my_bool

당신은 한 부분적 으로 컴파일러에서이 실행 경로에 UB을 "숨겨진"noinline . 인라인이 아니더라도 절차 간 최적화는 여전히 다른 함수의 정의에 의존하는 함수의 버전을 만들 수 있습니다. (먼저 clang은 기호 삽입이 발생할 수있는 Unix 공유 라이브러리가 아닌 실행 파일을 만들고 있습니다. 둘째, 정의 내부의 class{}정의이므로 모든 번역 단위는 동일한 정의를 가져야합니다. inline키워드 와 마찬가지로 )

따라서 실행 경로가 불가피하게 정의되지 않은 동작을 만나기 때문에 컴파일러는에 대한 정의로 ret또는 ud2(잘못된 명령)을 방출 할 수 있습니다. mainmain(인라인이 아닌 생성자를 통해 경로를 따르기로 결정한 경우 컴파일러가 컴파일 타임에 볼 수있는 동안)

UB를 만나는 모든 프로그램은 전체 존재에 대해 완전히 정의되지 않았습니다. 그러나 if()실제로 실행되지 않는 함수 또는 분기 내부의 UB 는 나머지 프로그램을 손상시키지 않습니다. 실제로 이것은 ret컴파일 타임에 UB를 포함하거나 이끌어 낼 수있는 전체 기본 블록에 대해 컴파일러가 잘못된 명령 또는을 방출하거나 아무것도 방출하지 않고 다음 블록 / 함수에 빠질 수 있음을 의미합니다.

실제로 GCC와 Clang은 실제로 ud2 말이되지 않는 실행 경로에 대한 코드를 생성하는 대신 실제로 UB에서 방출 합니다. 또는 비 void기능 종료에서 벗어나는 경우 gcc는 때때로 ret명령어를 생략합니다 . "내 기능이 RAX에있는 쓰레기와 함께 반환 될 것"이라고 생각했다면, 잘못 알고 있습니다. 최신 C ++ 컴파일러는 언어를 더 이상 이식 가능한 어셈블리 언어처럼 취급하지 않습니다. 독립형 비 인라인 버전의 함수가 asm으로 보이는 방법에 대한 가정없이 프로그램이 실제로 유효한 C ++이어야합니다.

또 다른 재미있는 예는 AMD64에서 mmap'ed 메모리에 대한 정렬되지 않은 액세스가 때때로 segfault 인 이유무엇입니까? . x86은 정렬되지 않은 정수에서 오류가 발생하지 않습니다. 왜 잘못 정렬 된 uint16_t*것이 문제가 될까요? 왜냐하면 alignof(uint16_t) == 2SSE2로 자동 벡터화 할 때이 가정을 위반하면 segfault가 발생하기 때문입니다.

clang 개발자의 기사 인 정의되지 않은 동작 # 1 / 3에 대해 모든 C 프로그래머가 알아야 할 사항 도 참조하십시오 .

요점 : 컴파일러가 컴파일 타임에 UB를 발견 한 경우 비트 패턴이 유효한 객체 표현 인 ABI를 대상으로하는 경우에도 UB를 발생시키는 코드를 통해 경로를 "파손"(놀람 한 asm을 방출 할 수 있음 ) 할 bool 있습니다.

프로그래머가 많은 실수, 특히 현대 컴파일러가 경고하는 것들에 대한 적대감이 예상됩니다. 따라서 -Wall경고를 사용 하고 수정 해야합니다 . C ++는 사용자에게 친숙한 언어가 아니며 컴파일하려는 대상에 asm으로 안전하더라도 C ++의 안전하지 않을 수 있습니다. (예를 들어, 부호있는 오버플로는 C ++에서 UB이며 컴파일러는을 사용하지 않으면 2의 보수 x86을 컴파일 할 때도 발생하지 않는다고 가정합니다 clang/gcc -fwrapv.)

컴파일 타임에 보이는 UB는 항상 위험하며, 링크 타임 최적화를 통해 컴파일러에서 UB를 실제로 숨겨서 어떤 종류의 asm이 생성되는지에 대해 추론 할 수는 없습니다.

지나치게 과격하지 않아야합니다. 종종 컴파일러는 무언가를 피하고 UB 일 때도 예상대로 코드를 내 보냅니다. 그러나 컴파일러가 값 범위에 대해 더 많은 정보를 얻는 최적화를 구현하면 미래에 문제가 될 수 있습니다 (예 : 변수가 음수가 아닌 경우 x86에서 0 확장을 해제하도록 부호 확장을 최적화 할 수 있음) 64). 예를 들어, 현재 gcc 및 clang에서 수행 tmp = a+INT_MINa<0항상 거짓으로 최적화되지 않으며 tmp항상 음수입니다. ( 이 2의 보수 목표에서 INT_MIN+ a=INT_MAX는 음수 이므로 a그보다 높을 수 없습니다.)

따라서 gcc / clang은 현재 계산 된 입력에 대한 범위 정보를 도출하기 위해 역 추적하지 않으며 부호있는 오버플로가 없다는 가정 ( : Godbolt)을 기반으로 한 결과 에 대해서만 추적 합니다. 이것이 최적화인지 사용자 친화의 이름으로 의도적으로 "누락 된"것인지 또는 무엇인지 모르겠습니다.

또한 구현 (일명 컴파일러)은 ISO C ++가 정의되지 않은 상태로 동작을 정의 할 수 있습니다 . 예를 들어, Intel의 내장 함수 ( _mm_add_ps(__m128, __m128)수동 SIMD 벡터화 와 같은) 를 지원하는 모든 컴파일러는 잘못 정렬 된 포인터를 형성 할 수 있어야합니다.이 포인터는 역 참조 하지 않아도 C ++에서 UB입니다 . __m128i _mm_loadu_si128(const __m128i *)잘못 정렬 복용하여 정렬되지 않은로드를 수행 __m128i*하지 않는, 인수를 void*하거나 char*. 하드웨어 벡터 포인터와 해당 유형 사이의`reinterpret_cast`는 정의되지 않은 동작입니까?

GNU C / C ++는 -fwrapv일반적인 부호있는 오버플로 UB 규칙과 별도로 음의 부호있는 숫자 (조차없는 ) 를 왼쪽으로 이동시키는 동작을 정의합니다 . ( 이것은 ISO C ++에서 UB 이며 부호있는 숫자의 오른쪽 시프트는 구현 정의 (논리 대 산술)입니다. 좋은 품질의 구현은 HW에서 산술 오른쪽 시프트가있는 산술을 선택하지만 ISO C ++은 지정하지 않습니다). 이 내용은 G 표준 매뉴얼의 정수 섹션에 문서화되어 있으며 C 표준은 구현 방식에 따라 구현 방법이 필요합니다.

컴파일러 개발자들이 염려하는 구현 품질 문제는 분명히 있습니다. 그들은 일반적으로 의도적으로 적대적인 컴파일러를 만들 려고 시도 하지 않지만 C ++의 모든 UB 움푹 들어간 곳 (정의 된 것을 제외하고)을 사용하여 더 잘 최적화하는 것은 때때로 거의 구별 할 수 없습니다.


각주 1 : 상위 56 비트는 일반적으로 레지스터보다 좁은 유형의 경우 수신자가 무시해야하는 가비지 일 수 있습니다.

( 다른 ABI 여기에서 다른 선택을합니다 . 일부는 MIPS64 및 PowerPC64와 같이 함수로 전달되거나 함수에서 반환 될 때 레지스터를 채우기 위해 좁은 정수 유형을 0 또는 부호 확장해야합니다. 이 x86-64 답변 의 마지막 섹션을 참조하십시오. 이전 ISA와 비교 한 것 입니다.)

예를 들어, 호출자는 a & 0x01010101호출하기 전에 RDI에서 계산 하여 다른 용도로 사용 했을 수 있습니다 bool_func(a&1). 호출자는의 &1일부로 이미 하위 바이트를 수행했기 때문에 최적화 할 수 and edi, 0x01010101있으며, 수신자가 상위 바이트를 무시해야한다는 것을 알고 있습니다.

또는 부울이 3 번째 인수로 전달되면 코드 크기를 최적화하는 호출자가 mov dl, [mem]대신 대신 로드하여 movzx edx, [mem]RDX의 이전 값 (또는 다른 부분 레지스터 효과에 따라 잘못된 의존성으로 1 바이트를 절약 할 수 있음) CPU 모델). 또는 어쨌든 REX 접두사가 필요하기 때문에 , mov dil, byte [r10]대신 첫 번째 인수에 대해 movzx edi, byte [r10].

이것은 왜 그 소리를 방출이다 movzx eax, dil에서 Serialize대신, sub eax, edi. 정수 인수의 경우 clang은 문서화되지 않은 gcc 및 clang의 동작에 따라 좁은 정수를 32 비트로 0 또는 부호 확장하거나 32 비트 오프셋을 포인터에 추가 할 때 부호 또는 0 확장이 필요합니다. x86-64 ABI? 그래서 나는 그것이 같은 일을하지 않는다는 것을 알고 싶어했습니다 bool.)


각주 2 : 분기 후에는 4 바이트 mov즉석 또는 4 바이트 + 1 바이트 저장소 만 있으면 됩니다. 길이는 상점 너비 + 오프셋에 내재되어 있습니다.

OTOH, glibc memcpy는 길이에 따라 겹치는 두 개의 4 바이트로드 / 스토어를 수행하므로 실제로는 부울의 모든 조건부 분기가 없어집니다. glibc의 memcpy / memmove에 있는 L(between_4_7):블록 을 보십시오 . 또는 적어도 memcpy의 boolean에서 청크 크기를 선택하는 것과 동일한 방식으로 진행하십시오.

인라인 인 경우 2x mov-immediate + cmov와 조건부 오프셋을 사용하거나 문자열 데이터를 메모리에 남겨 둘 수 있습니다.

또는 Intel Ice Lake ( Fast Short REP MOV 기능 사용 )를 튜닝하는 경우 실제 성능 rep movsb이 최적 일 수 있습니다. glibc memcpyrep movsb 이 기능을 갖춘 CPU에서 작은 크기로 사용 하기 시작하여 많은 분기를 절약 할 수 있습니다.


UB 감지 및 초기화되지 않은 값 사용을위한 도구

gcc 및 clang에서는 -fsanitize=undefined런타임에 발생하는 UB에서 경고 또는 오류가 발생하는 런타임 계측을 추가하기 위해 컴파일 할 수 있습니다 . 그러나 그것은 단일화 된 변수를 잡을 수 없습니다. "초기화되지 않은"비트를위한 공간을 만들기 위해 유형 크기를 늘리지 않기 때문입니다.

https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/을 참조 하십시오

초기화되지 않은 데이터의 사용법을 찾으려면 clang / LLVM에 Address Sanitizer 및 Memory Sanitizer가 있습니다. https://github.com/google/sanitizers/wiki/MemorySanitizerclang -fsanitize=memory -fPIE -pie초기화되지 않은 메모리 읽기 를 감지하는 예를 보여줍니다 . 최적화 없이 컴파일하면 가장 잘 작동 할 수 있으므로 모든 변수 읽기는 실제로 asm의 메모리에서로드됩니다. -O2부하가 최적화되지 않는 경우 에 사용됨을 나타 냅니다. 나는 그것을 직접 시도하지 않았다. (어떤 경우, 배열을 합치기 전에 누산기를 초기화하지 않으면 clang -O3는 초기화되지 않은 벡터 레지스터에 합산되는 코드를 생성합니다. 따라서 최적화를 통해 UB와 관련된 메모리 읽기가없는 경우가 있습니다 하지만-fsanitize=memory 생성 된 asm을 변경하면이를 확인할 수 있습니다.)

초기화되지 않은 메모리의 복사와 간단한 논리 및 산술 연산을 허용합니다. 일반적으로 MemorySanitizer는 메모리에 초기화되지 않은 데이터의 확산을 자동으로 추적하고 초기화되지 않은 값에 따라 코드 분기를 수행하거나 수행하지 않을 때 경고를보고합니다.

MemorySanitizer는 Valgrind (Memcheck 도구)에있는 일부 기능을 구현합니다.

호출이 glibc가 있기 때문에 그것은이 경우에 작동합니다 memcpy으로 length초기화되지 않은 메모리 계산이 지점에서 결과 (도서관 내부)에 기반합니다 length. 방금 cmov, 인덱싱 및 두 개의 저장소를 사용한 완전히 분기없는 버전을 인라인 한 경우 작동하지 않았을 수 있습니다.

Valgrindmemcheck 는 또한 이런 종류의 문제를 찾아서 프로그램이 초기화되지 않은 데이터를 단순히 복사하는지에 대해 불평하지 않습니다. 그러나 그것은 초기화되지 않은 데이터에 의존하는 외부에서 보이는 행동을 포착하기 위해 "조건부 점프 또는 이동이 초기화되지 않은 값에 의존하는 경우"를 감지 할 것이라고 말합니다.

아마도로드에 플래그를 지정하지 않는 배후의 아이디어는 구조체에 패딩이있을 수 있으며 개별 구조체가 한 번에 하나씩 만 작성된 경우에도 전체 벡터로드 / 스토어로 전체 구조체 (패딩 포함)를 복사하는 것은 오류가 아니라는 것입니다. asm 레벨에서 패딩 된 내용과 실제로 값의 일부에 대한 정보가 손실되었습니다.


2
변수가 8 비트 정수 범위가 아니라 전체 CPU 레지스터의 값을 취하는 더 나쁜 경우를 보았습니다. 그리고 Itanium은 아직 더 나빠서 초기화되지 않은 변수를 사용하면 완전히 충돌 할 수 있습니다.
Joshua

2
@Joshua : 오, 좋은 지적, Itanium의 명시적인 추측은 레지스터 값에 "숫자가 아님"과 같은 값으로 태그를 지정하여 값 오류를 사용합니다.
피터 코 데스

11
또한,이 또한 보여 UB의의 featurebug는 처음에 언어 C 및 C ++의 설계에 도입 : 그것은 컴파일러 제공하기 때문에 정확하게 이러한 높은 품질을 수행하는 가장 현대적인 컴파일러를 허용 지금 한 자유의이 종류를, C / C ++를 고성능 중급 언어로 만드는 최적화.
The_Sympathizer

2
따라서 유용한 프로그램을 작성하려는 C ++ 컴파일러 작성자와 C ++ 프로그래머 간의 전쟁이 계속되고 있습니다. 이 질문에 대답하는 데있어 포괄적 인이 답변은 정적 분석 도구 공급 업체를위한 설득력있는 광고 카피처럼 사용될 수 있습니다.
davidbak

4
@The_Sympathizer : UB는 고객에게 가장 유용한 방식으로 구현을 구현할 수 있도록 포함되었습니다 . 모든 행동이 똑같이 유용한 것으로 간주되도록 제안하는 것은 아닙니다.
supercat

56

컴파일러는 인수로 전달 부울 값이 유효한 부울 값 (초기화하거나 변환 된 즉, 하나 있다고 가정 할 수있다 true또는 false). true, 참으로 여러 가지의 표현이있을 수 있습니다 - 값은 정수 1과 동일하지 않아도 truefalse-하지만 매개 변수는 "올바른 표현은"구현 - 인 두 값 중 하나의 일부 유효한 표현해야합니다 한정된.

따라서를 초기화하지 못 bool하거나 다른 유형의 포인터를 통해 덮어 쓰면 컴파일러의 가정이 잘못되고 정의되지 않은 동작이 발생합니다. 경고를 받았습니다 :

50) 초기화되지 않은 자동 객체의 값을 검사하는 것과 같이이 국제 표준에서 "정의되지 않음"으로 설명 된 방식으로 부울 값을 사용하면 마치 true 또는 false가 아닌 것처럼 동작 할 수 있습니다. (기본 유형 §6.9.1의 6 항에 대한 각주)


11
" true값이 정수 1과 같을 필요는 없습니다"는 오해의 소지가 있습니다. 물론, 실제 비트 패턴은 뭔가 다른, 그러나 암시 적으로 변환 할 때 / 승진 (당신이 아닌 다른 값으로 볼 수있을 유일한 방법은 true/는 false) true항상 1, 그리고 false항상0 . 물론 그러한 컴파일러는이 컴파일러가 사용하려고했던 트릭을 사용할 수 없으므로 ( bool실제 비트 패턴은 0또는 만 가능 하다는 사실을 사용하여 1) OP의 문제와 관련이 없습니다.
ShadowRanger 2016 년

4
@ShadowRanger 항상 객체 표현을 직접 검사 할 수 있습니다.
TC

7
@ shadowranger : 내 요점은 구현이 담당한다는 것입니다. true비트 패턴 의 유효한 표현을 제한하면 1특권입니다. 다른 표현 세트를 선택하면 실제로 여기에 표시된 최적화를 사용할 수 없습니다. 특정 표현을 선택하면 가능합니다. 내부적으로 만 일치하면됩니다. a를 바이트 배열로 복사 하여 표현을 조사 있습니다bool . 그것은 UB가 아닙니다 (그러나 그것은 구현에 의해 정의됩니다)
rici

3
그렇습니다. 컴파일러 최적화 (즉, 실제 C ++ 구현)는 종종 bool비트 패턴 0또는 에 따라 코드를 생성합니다 1. bool메모리에서 읽을 때마다 (또는 arg 함수를 보유한 레지스터) 다시 부울하지 않습니다 . 이것이 바로이 답변의 말입니다. 예는 : gcc4.7 +는 최적화 return a||bor eax, edi반환하는 함수 bool, 또는 MSVC는 최적화 a&btest cl, dl. x86 test비트 단위 and 이므로 if cl=1dl=2test는에 따라 플래그를 설정합니다 cl&dl = 0.
Peter Cordes

5
정의되지 않은 동작 에 대한 요점은 컴파일러가 그것에 대해 훨씬 더 많은 결론을 도출 할 수 있다는 것입니다. . 따라서 낮은 수준의 값이 0 또는 1과 다를 가능성은 없습니다.
Holger

52

함수 자체는 정확하지만 테스트 프로그램에서 함수를 호출하는 명령문은 초기화되지 않은 변수의 값을 사용하여 정의되지 않은 동작을 유발합니다.

버그는 호출 함수에 있으며 코드 검토 또는 호출 함수의 정적 분석에 의해 감지 될 수 있습니다. gcc 8.2 컴파일러는 컴파일러 탐색기 링크를 사용하여 버그를 감지합니다. (아마도 문제를 찾지 못한다는 버그에 대해 버그 보고서를 제출할 수 있습니다).

정의되지 않은 동작은 정의되지 않은 동작 을 트리거 한 이벤트 후 몇 줄이 충돌하는 것을 포함하여 모든 일이 발생할 수 있음을 의미 합니다.

NB. "정의되지 않은 동작으로 인해 _____가 발생할 수 있습니까?" 항상 "예"입니다. 그것은 말 그대로 정의되지 않은 행동의 정의입니다.


2
첫 번째 조항이 사실입니까? 단지 않습니다 복사 초기화되지 않은 bool트리거 UB를?
Joshua Green

10
@JoshuaGreen 참조 [dcl.init] / 12 "평가에 의해 결정되지 않은 값이 생성되는 경우, 다음 경우를 제외하고는 동작이 정의되지 않습니다 :"(그리고 그러한 경우에는 예외가 없습니다 bool). 복사는 소스를 평가해야합니다
MM

8
@JoshuaGreen 그리고 그 이유는 일부 유형의 일부 유효하지 않은 값에 액세스하면 하드웨어 결함을 트리거하는 플랫폼이있을 수 있기 때문입니다. 이를 "트랩 표현"이라고도합니다.
David Schwartz

7
Itanium은 애매하지만 여전히 프로덕션 상태이고 트랩 값을 가지며 최소 반 현대 C ++ 컴파일러 (Intel / HP)가 2 개있는 CPU입니다. 문자 그대로 true, falsenot-a-thing부울 값이 있습니다.
MSalters

3
반면에, 표준은 모든 컴파일러가 특정 방식으로 무언가를 처리하도록 요구하고 있습니까?에 대한 대답은 일반적으로 "아니오"입니다. 심지어 모든 품질 컴파일러가 그렇게해야한다는 것이 명백한 경우에도 마찬가지입니다. 더 분명한 것은 표준 작성자가 실제로 말할 필요가 적을 것입니다.
supercat

23

부울은 trueand에 내부적으로 사용 된 구현 종속 값만 보유 false할 수 있으며 생성 된 코드는이 두 값 중 하나만 보유한다고 가정 할 수 있습니다.

일반적으로, 구현은 정수를 사용 0하기위한 false1대한 true사이의 전환을 단순화하기 위해, bool그리고 int및 확인 if (boolvar)과 같은 코드를 생성 if (intvar). 이 경우 할당에서 삼항에 대해 생성 된 코드가 값을 두 문자열에 대한 포인터 배열의 인덱스로 사용한다고 상상할 수 있습니다. 즉, 다음과 같이 변환 될 수 있습니다.

// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

boolValue초기화되지 않은 경우 실제로 정수 값을 보유 할 수 있으며 이로 인해 strings배열 경계 외부에서 액세스 할 수 있습니다 .


1
@ 감사합니다. 이론적으로 내부 표현은 정수로 /에서 캐스트하는 방법과 반대 일 수 있지만 그 반대 일 수 있습니다.
Barmar

1
당신이 옳고, 당신의 모범도 무너질 것입니다. 그러나 초기화되지 않은 변수를 배열의 인덱스로 사용하는 것은 코드 검토에 "표시"됩니다. 또한 디버그에서도 충돌이 발생합니다 (예 : 일부 디버거 / 컴파일러는 특정 패턴으로 초기화되어 충돌시기를 쉽게 확인할 수 있음). 내 예에서 놀라운 부분은 bool의 사용법이 보이지 않는다는 것입니다. 옵티마이 저는 소스 코드에없는 계산에이를 사용하기로 결정했습니다.
Remz

3
@Remz 나는 배열을 사용하여 생성 된 코드가 무엇과 동등한지를 보여 주므로 누군가가 실제로 작성할 것이라고 제안하지는 않습니다.
Barmar

1
@Remz boolint사용 하여 로 캐스트 *(int *)&boolValue하고 디버깅 목적으로 인쇄하고 그것이 아닌지 0또는 1충돌 하는지 확인하십시오 . 그렇다면 컴파일러가 인라인 -if를 배열로 최적화하고 있다는 이유를 설명합니다.
Havenard

2
@MSalters : std::bitset<8>다른 모든 플래그의 이름을 밝히지 않습니다. 그들이 무엇인지에 따라, 그것은 중요 할 수 있습니다.
Martin Bonner는 Monica

15

질문을 많이 요약하면 C ++ 표준을 사용하여 컴파일러가 bool내부 숫자 표현이 '0'또는 '1'인 것으로 가정하여 그러한 방식으로 사용할 수 있습니까?

표준은의 내부 표현에 대해서는 아무 것도 말하지 않습니다 bool. a bool로 캐스팅 할 때 발생하는 상황 만 정의 합니다 int(또는 그 반대). 대부분 이러한 통합 변환 (및 사람들이 그에 크게 의존한다는 사실) 때문에 컴파일러는 0과 1을 사용하지만 반드시 사용할 필요는 없습니다 (하지만 사용하는 하위 레벨 ABI의 제약 조건을 존중해야 함) ).

따라서 컴파일러는 a를 볼 때 ' '또는 ' '비트 패턴 중 하나를 포함 하고 느낌이 드는 것을 수행 할 것을 bool고려할 자격이 bool있습니다 . 따라서 및 의 값이 각각 1과 0 인 경우 컴파일러는에 최적화 할 수 있습니다. 다른 재미있는 행동이 가능합니다!truefalsetruefalsestrlen5 - <boolean value>

여기에 반복적으로 언급 된 것처럼 정의되지 않은 동작은 정의되지 않은 결과를 갖습니다. 포함하지만 이에 국한되지는 않습니다

  • 예상대로 작동하는 코드
  • 임의의 시간에 코드 실패
  • 코드가 전혀 실행되지 않습니다.

정의되지 않은 동작에 대해 모든 프로그래머가 알아야 할 사항 보기

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.