루프의 어느 지점에서 정수 오버플로가 정의되지 않은 동작이됩니까?


86

이것은 여기에 게시 할 수없는 훨씬 더 복잡한 코드를 포함하는 내 질문을 설명하는 예입니다.

#include <stdio.h>
int main()
{
    int a = 0;
    for (int i = 0; i < 3; i++)
    {
        printf("Hello\n");
        a = a + 1000000000;
    }
}

이 프로그램 a은 세 번째 루프에서 오버플로 되기 때문에 내 플랫폼에서 정의되지 않은 동작을 포함 합니다.

그러면 전체 프로그램 이 정의되지 않은 동작을 갖게 됩니까 ? 아니면 오버플로가 실제로 발생한 후에야 합니까? 컴파일러는 잠재적으로는 해결할 수 없습니다 a 이 정의되지 않은 전체 루프를 선언하고 그들은 모두 오버 플로우 전에 발생하더라도 printfs을 실행하는 데 방해 할 수 없도록 오버 플로우?

(태그가 지정된 C와 C ++는 다르지만 두 언어가 다르면 두 언어에 대한 답변에 관심이 있기 때문에 다릅니다.)


7
컴파일러가 a(자체 계산을 제외하고) 사용되지 않는 것을 알아 내고 간단히 제거 할 수 있는지 궁금합니다a
4386427

12
올해 CppCon의 My Little Optimizer : Undefined Behavior is Magic 을 즐길 수 있습니다 . 정의되지 않은 동작을 기반으로 컴파일러가 수행 할 수있는 최적화에 관한 것입니다.
TartanLlama



답변:


108

순전히 이론적 인 답변에 관심이 있다면 C ++ 표준은 정의되지 않은 동작을 "시간 여행"에 허용합니다.

[intro.execution]/5: 잘 구성된 프로그램을 실행하는 준수 구현은 동일한 프로그램 및 동일한 입력을 사용하여 추상 기계의 해당 인스턴스의 가능한 실행 중 하나와 동일한 관찰 가능한 동작을 생성해야합니다. 그러나 그러한 실행에 정의되지 않은 작업이 포함되어있는 경우이 국제 표준은 해당 입력으로 해당 프로그램을 실행하는 구현에 대한 요구 사항을 지정하지 않습니다 (첫 번째 정의되지 않은 작업 이전의 작업에 대해서도).

따라서 프로그램에 정의되지 않은 동작이 포함되어 있으면 전체 프로그램 의 동작 이 정의되지 않습니다.


4
@KeithThompson :하지만 sneeze()함수 자체는 클래스 Demon(비강 다양성이 하위 클래스 임)의 모든 클래스에서 정의되지 않아 어쨌든 모든 것이 순환되도록 만듭니다.
Sebastian Lenartowicz 2016 년

1
그러나 printf가 반환되지 않을 수 있으므로 처음 두 라운드가 정의됩니다. 완료 될 때까지 UB가 있다는 것이 명확하지 않기 때문입니다. stackoverflow.com/questions/23153445/…
usr

1
(부트 스트랩 코드가 정의되지 않은 동작에 의존하기 때문에) 컴파일러는 리눅스 커널에 대한 "NOP"이 방출하는 권리 내에서 기술적 이유입니다 : blog.regehr.org/archives/761
Crashworks

3
@Crashworks 그리고 이것이 Linux가 이식 불가능한 C 로 작성되고 컴파일되는 이유입니다 . (예 : -fno-strict-aliasing과 같은 특정 옵션이있는 특정 컴파일러가 필요한 C의 상위 집합)
user253751

3
@usr printf반환하지 않으면 정의되어 있지만 printf반환 할 경우 정의되지 않은 동작 printf이 호출 되기 전에 문제를 일으킬 수 있습니다 . 따라서 시간 여행. printf("Hello\n");그리고 다음 줄은 다음과 같이 컴파일됩니다undoPrintf(); launchNuclearMissiles();
user253751

31

먼저이 질문의 제목을 수정하겠습니다.

정의되지 않은 동작은 (특히) 실행 영역이 아닙니다.

정의되지 않은 동작은 컴파일, 링크,로드 및 실행과 같은 모든 단계에 영향을줍니다.

이를 확고히하기위한 몇 가지 예는 완전한 섹션이 없다는 것을 명심하십시오.

  • 컴파일러는 Undefined Behavior가 포함 된 코드 부분이 실행되지 않는다고 가정 할 수 있으므로 이러한 동작으로 연결되는 실행 경로가 데드 코드라고 가정합니다. 참조 모든 C 프로그래머가 정의되지 않은 동작에 대해 알아야 할 사항 Chris Lattner 외에는 에 .
  • 링커는 약한 기호 (이름으로 인식됨)의 여러 정의가있는 경우 단일 정의 규칙 덕분에 모든 정의가 동일하다고 가정 할 수 있습니다.
  • 로더 (동적 라이브러리를 사용하는 경우)는 동일하다고 가정 할 수 있으므로 찾은 첫 번째 기호를 선택합니다. 이것은 일반적으로 (ab) LD_PRELOADUnix에서 트릭을 사용하여 호출을 가로채는 데 사용됩니다.
  • 매달린 포인터를 사용하면 실행이 실패 할 수 있습니다 (SIGSEV).

이것이 Undefined Behavior에 대해 너무나 무서운 것입니다. 정확한 동작이 발생하는지 미리 예측하는 것은 거의 불가능하며,이 예측은 도구 체인, 기본 OS 등이 업데이트 될 때마다 재검토되어야합니다.


Michael Spencer (LLVM 개발자) : CppCon 2016 : My Little Optimizer : Undefined Behavior is Magic 이 비디오를 시청하는 것이 좋습니다 .


3
이것이 나를 걱정하는 것입니다. 내 실제 코드에서는 복잡하지만 항상 오버플로되는 경우 가있을 수 있습니다. 그리고 저는 그것에 대해별로 신경 쓰지 않지만 "올바른"코드도 이것에 의해 영향을받을 것이라고 걱정합니다. 분명히 나는 그것을 해결하기 위해 필요하지만, 정부는 :) 이해가 필요합니다
jcoder

8
@jcoder : 여기에 중요한 탈출구가 하나 있습니다. 컴파일러는 입력 데이터를 추측 할 수 없습니다. 정의되지 않은 동작이 발생하지 않는 입력이 하나 이상있는 한 컴파일러는이 특정 입력이 여전히 올바른 출력을 생성하는지 확인해야합니다. 위험한 최적화에 대한 모든 무서운 이야기는 피할 수없는 UB 에만 적용됩니다 . 실제로 argc루프 카운트로 사용했다면 케이스 argc=1는 UB를 생성하지 않으며 컴파일러는이를 처리해야합니다.
MSalters

@jcoder :이 경우 이것은 데드 코드가 아닙니다. 그러나 컴파일러는 i여러 N번 증가 할 수 없으며 따라서 그 값이 제한되어 있음 을 추론 할 수 있을만큼 똑똑 할 수 있습니다 .
Matthieu M.

4
@jcoder 다음과 같은 경우는 f(good);어떤 일이 X을 수행하고 f(bad);단지를 발동는 것을 정의되지 않은 동작, 다음 프로그램 호출 f(good);X을 보장을하지만, f(good); f(bad);X. 할 보장 할 수 없습니다

4
@Hurkyl 더 흥미롭게도 코드가 if(foo) f(good); else f(bad);이면 스마트 컴파일러는 비교를 버리고 무조건 foo(good).
John Dvorak 16.

28

16 비트 int를 대상으로하는 적극적으로 최적화 된 C 또는 C ++ 컴파일러 는 유형 에 추가 할 때 의 동작 이 정의되지 않음알게 됩니다 .1000000000int

이 원하는 무엇이든 할 하나의 표준으로 허용 할 수 떠나, 전체 프로그램의 삭제를 포함를 int main(){}.

그러나 더 큰 ints는 어떻습니까? 이 작업을 수행하는 컴파일러는 아직 모릅니다 (그리고 저는 C 및 C ++ 컴파일러 설계 전문가가 아닙니다).하지만 때때로 32 비트 int이상을 대상으로하는 컴파일러 가 루프를 알아낼 것이라고 생각합니다. 무한 ( i변경되지 않음) 너무 a것이다 결국 오버 플로우. 따라서 다시 한 번 출력을 int main(){}. 내가 여기서 말하고자하는 요점은 컴파일러 최적화가 점진적으로 더 공격적이됨에 따라 점점 더 정의되지 않은 동작 구조가 예기치 않은 방식으로 나타나고 있다는 것입니다.

루프가 무한하다는 사실은 루프 본문의 표준 출력에 쓰고 있기 때문에 그 자체로 정의되지 않은 것은 아닙니다.


3
정의되지 않은 동작이 나타나기 전에도 원하는 것을 할 수 있도록 표준에 의해 허용됩니까? 이것은 어디에 명시되어 있습니까?
jimifiki

4
왜 16 비트입니까? OP가 32 비트 부호있는 오버플로를 찾고 있다고 생각합니다.
4386427 '1610.07.10

8
@jimifiki 표준입니다. C ++ 14 (N4140) 1.3.24 "정확한 동작 =이 국제 표준이 요구 사항을 부과하지 않는 동작." 게다가 정교하게 설명하는 긴 메모. 그러나 요점은 정의되지 않은 "문"의 동작이 아니라 프로그램 의 동작이라는 것 입니다. 즉, UB가 표준의 규칙에 의해 (또는 규칙이없는 경우) 트리거되는 한 표준이 프로그램 전체 에 적용되는 것을 중단합니다 . 따라서 프로그램의 모든 부분이 원하는대로 작동 할 수 있습니다.
문헌 : Angew는 더 이상 자랑 SO의없는

5
첫 번째 진술은 잘못되었습니다. 경우 int16 비트이고, 추가로이 열린다 long(리터럴 피연산자 타입을 갖기 때문에 long)이 잘 정의 된 것 여기서, 다음에 구현 정의 다시 변환하여 변환 int.
R .. GitHub STOP HELPING ICE

2
@usr의 동작은 printf항상 반환하도록 표준에 의해 정의됩니다
MM

11

기술적으로 C ++ 표준에서 프로그램에 정의되지 않은 동작이 포함 된 경우 전체 프로그램의 동작, 컴파일 타임 (프로그램이 실행되기 전) 에서도 이 정의되지 않습니다.

실제로 컴파일러는 오버플로가 발생하지 않을 것이라고 (최적화의 일부로) 가정 할 수 있기 때문에 적어도 루프의 세 번째 반복 (32 비트 머신 가정)에서 프로그램의 동작은 정의되지 않습니다. 세 번째 반복 전에 올바른 결과를 얻을 가능성이 높습니다. 그러나 전체 프로그램의 동작이 기술적으로 정의되지 않았기 때문에 프로그램이 완전히 잘못된 출력 (출력 없음 포함)을 생성하거나 실행 중 어느 시점에서든 런타임에 충돌하거나 심지어 컴파일에 실패하는 것을 막을 수 없습니다 (정의되지 않은 동작이 다음과 같이 확장 됨). 컴파일 시간).

정의되지 않은 동작은 코드가 수행해야하는 작업에 대한 특정 가정을 제거하므로 컴파일러에 더 많은 최적화 공간을 제공합니다. 이렇게하면 정의되지 않은 동작과 관련된 가정에 의존하는 프로그램이 예상대로 작동하지 않을 수 있습니다. 따라서 C ++ 표준에 따라 정의되지 않은 것으로 간주되는 특정 동작에 의존해서는 안됩니다.


UB 부분이 if(false) {}범위 내에 있으면 어떻게됩니까? 컴파일러가 모든 분기에 ~ 잘 정의 된 논리 부분이 포함되어 있다고 가정하고 잘못된 가정으로 작동한다고 가정하기 때문에 전체 프로그램이 중독됩니까?
mlvljr

1
표준은 정의되지 않은 동작에 대해 어떠한 요구 사항도 부과하지 않으므로 이론적으로 는 전체 프로그램을 독살합니다. 그러나 실제로 최적화 컴파일러는 죽은 코드를 제거 할 가능성이 있으므로 실행에 영향을 미치지 않을 것입니다. 그래도이 동작에 의존해서는 안됩니다.
bwDraco

좋은 고맙습니다 :), 알고
mlvljr

9

@TartanLlama가 적절하게 표현한 것처럼 정의되지 않은 동작이 '시간 여행'이 가능한 이유 를 이해하기 위해 'as-if'규칙을 살펴 보겠습니다.

1.9 프로그램 실행

1 이 국제 표준의 의미 설명은 매개 변수화 된 비 결정적 추상 기계를 정의합니다. 이 국제 표준은 준수 구현의 구조에 대한 요구 사항을 지정하지 않습니다. 특히 추상 기계의 구조를 복사하거나 에뮬레이트 할 필요가 없습니다. 오히려, 아래에 설명 된 것처럼 추상 기계의 관찰 가능한 동작을 에뮬레이트하기 위해서는 준수 구현이 필요합니다.

이를 통해 프로그램을 입력과 출력이있는 '블랙 박스'로 볼 수 있습니다. 입력은 사용자 입력, 파일 및 기타 여러 가지가 될 수 있습니다. 출력은 표준에 언급 된 '관찰 가능한 동작'입니다.

표준은 입력과 출력 간의 매핑 만 정의합니다. '예제 블랙 박스'를 설명하여이를 수행하지만 동일한 매핑을 가진 다른 모든 블랙 박스가 똑같이 유효하다고 명시 적으로 말합니다. 이것은 블랙 박스의 내용이 무관하다는 것을 의미합니다.

이를 염두에두고 정의되지 않은 동작이 특정 순간에 발생한다고 말하는 것은 이치에 맞지 않습니다. 블랙 박스 의 샘플 구현에서 언제 어디서 발생하는지 말할 수 있지만 실제 블랙 박스는 완전히 다른 것일 수 있으므로 더 이상 어디서, 언제 발생하는지 말할 수 없습니다. 이론적으로 컴파일러는 예를 들어 가능한 모든 입력을 열거하고 결과 출력을 미리 계산하도록 결정할 수 있습니다. 그러면 컴파일 중에 정의되지 않은 동작이 발생했을 것입니다.

정의되지 않은 동작은 입력과 출력 간의 매핑이 존재하지 않는 것입니다. 프로그램은 일부 입력에 대해 정의되지 않은 동작을 가질 수 있지만 다른 입력에 대해서는 정의 된 동작을 가질 수 있습니다. 그러면 입력과 출력 간의 매핑이 단순히 불완전합니다. 출력에 대한 매핑이없는 입력이 있습니다.
질문의 프로그램에는 입력에 대해 정의되지 않은 동작이 있으므로 매핑이 비어 있습니다.


6

int32 비트 라고 가정하면 정의되지 않은 동작이 세 번째 반복에서 발생합니다. 예를 들어 루프가 조건부로만 도달 할 수 있거나 세 번째 반복 전에 조건부로 종료 될 수있는 경우 실제로 세 번째 반복에 도달하지 않는 한 정의되지 않은 동작이 없습니다. 그러나 정의되지 않은 동작의 경우 정의되지 않은 동작 의 호출과 관련된 "과거"의 출력을 포함 하여 프로그램의 모든 출력 이 정의되지 않습니다. 예를 들어,이 경우 출력에 3 개의 "Hello"메시지가 표시된다는 보장이 없습니다.


6

TartanLlama의 대답이 맞습니다. 정의되지 않은 동작은 컴파일 시간 동안에도 언제든지 발생할 수 있습니다. 이것은 어리석은 것처럼 보일 수 있지만 컴파일러가 필요한 작업을 수행 할 수 있도록 허용하는 핵심 기능입니다. 컴파일러가되는 것이 항상 쉬운 것은 아닙니다. 매번 사양에 명시된대로 정확하게 수행해야합니다. 그러나 때로는 특정 행동이 발생하고 있음을 증명하는 것이 엄청나게 어려울 수 있습니다. 중지 문제를 기억한다면, 특정 입력이 공급 될 때 완료되었는지 또는 무한 루프에 들어 갔는지 증명할 수없는 소프트웨어를 개발하는 것은 다소 사소한 일입니다.

우리는 컴파일러를 비관적으로 만들고 다음 명령어가 문제와 같은 중단 문제 중 하나가 될 수 있다는 두려움에 끊임없이 컴파일 할 수 있지만 이는 합리적이지 않습니다. 대신 컴파일러에게 패스를 제공합니다. 이러한 "정의되지 않은 동작"주제에 대해 책임을지지 않습니다. 정의되지 않은 행동은 매우 미묘하게 사악한 행동들로 구성되어있어 우리가 정말로 지독하고 사악한 중지 문제와 그 밖의 것들로부터 분리하는 데 어려움을 겪습니다.

내가 게시하는 것을 좋아하는 예가 있지만 소스를 잃어 버렸다는 것을 인정하므로 의역을해야합니다. 특정 버전의 MySQL에서 가져온 것입니다. MySQL에서는 사용자가 제공 한 데이터로 채워진 순환 버퍼가 있습니다. 물론 그들은 데이터가 버퍼를 넘치지 않는지 확인하기를 원했기 때문에 확인했습니다.

if (currentPtr + numberOfNewChars > endOfBufferPtr) { doOverflowLogic(); }

충분히 정상인 것 같습니다. 그러나 numberOfNewChars가 정말 크고 오버플로되면 어떻게 될까요? 그런 다음 래핑되고보다 작은 포인터가 endOfBufferPtr되므로 오버플로 논리가 호출되지 않습니다. 그래서 그들은 그 전에 두 번째 수표를 추가했습니다.

if (currentPtr + numberOfNewChars < currentPtr) { detectWrapAround(); }

버퍼 오버플로 오류를 처리 한 것 같습니다. 그러나이 버퍼가 특정 버전의 데비안에서 오버플로되었다는 버그가 제출되었습니다! 신중한 조사에 따르면이 버전의 데비안은 특히 최신 버전의 gcc를 사용한 첫 번째 버전이었습니다. 이 버전의 gcc에서 컴파일러는 currentPtr + numberOfNewChars가 절대로 currentPtr보다 작은 포인터가 될 수 없다는 것을 인식했습니다. 포인터에 대한 오버플로는 정의되지 않은 동작이기 때문입니다! 그것은 gcc가 전체 검사를 최적화하기에 충분했고, 당신이 그것을 검사하기 위해 코드를 작성 했음에도 불구 하고 갑자기 버퍼 오버 플로우로부터 보호받지 못했습니다 !

이것은 사양 동작이었습니다. 모든 것이 합법적이었습니다 (내가들은 바에 따르면 gcc는 다음 버전에서이 변경 사항을 롤백했습니다). 직관적 인 행동이라고 생각하는 것은 아니지만 상상력을 조금만 확장하면이 상황의 약간의 변형이 어떻게 컴파일러의 중단 문제가 될 수 있는지 쉽게 알 수 있습니다. 이 때문에 사양 작성자는이를 "정의되지 않은 동작"으로 만들고 컴파일러가 원하는 모든 작업을 수행 할 수 있다고 말했습니다.


특히 x86에서 간단한 코드 생성을 수행 할 때도 중간을 자르는 것보다 더 효율적일 때가 있다는 점을 고려하면 부호있는 산술이 "int"를 초과하는 유형에 대해 수행되는 것처럼 동작하는 것처럼 작동하는 놀라운 컴파일러를 특별히 고려하지 않습니다. 결과. 더 놀라운 것은 오버플로가 다른 계산에 영향을 줄 때 코드가 uint16_t 값의 곱을 uint32_t에 저장하더라도 gcc에서 발생할 수 있다는 것입니다. 이는 비 정화 빌드에서 놀랍게 행동 할 그럴듯한 이유가 없어야합니다.
supercat

물론, if(numberOfNewChars > endOfBufferPtr - currentPtr)numberOfNewChars가 절대로 음수 일 수없고 currentPtr이 항상 버퍼 내의 어딘가를 가리키고 우스꽝스러운 "랩 어라운드"검사가 필요하지 않은 경우 올바른 검사는 입니다. (제공 한 코드가 순환 버퍼에서 작동 할 희망이 없다고 생각합니다. 의역에서 필요한 모든 것을
생략 했으므로이

@ Random832 나는 톤을 생략했습니다. 나는 더 큰 맥락을 인용하려고했지만, 출처를 잃어 버렸기 때문에 맥락을 의역하는 것이 더 문제가되는 것을 발견했기 때문에 그만 두었습니다. 나는 제대로 인용 할 수 있도록 그 폭발 한 버그 보고서를 찾아야합니다. 코드를 한 가지 방식으로 작성하고 완전히 다르게 컴파일 할 수있는 방법을 보여주는 강력한 예입니다.
Cort Ammon 2016 년

이것은 정의되지 않은 행동에 대한 가장 큰 문제입니다. 때로는 올바른 코드를 작성하는 것이 불가능하고 컴파일러가이를 감지하면 기본적으로 정의되지 않은 동작이 트리거되었음을 알리지 않습니다. 이 경우 사용자는 단순히 포인터를 사용하든 안하든 산술을 원하고 보안 코드를 작성하기위한 모든 노력이 취소되었습니다. 최소한 코드 섹션에 주석을 달 수있는 방법이 있어야합니다. 여기에는 멋진 최적화가 없습니다. C / C ++는 너무 많은 중요 영역에서 사용되어 이러한 위험한 상황이 최적화를 계속하도록 허용합니다
John McGrath

4

이론적 답변을 넘어서 실용적인 관찰은 오랫동안 컴파일러가 루프에 다양한 변환을 적용하여 루프 내에서 수행되는 작업의 양을 줄였다는 것입니다. 예를 들면 다음과 같습니다.

for (int i=0; i<n; i++)
  foo[i] = i*scale;

컴파일러는이를 다음과 같이 변환 할 수 있습니다.

int temp = 0;
for (int i=0; i<n; i++)
{
  foo[i] = temp;
  temp+=scale;
}

따라서 모든 루프 반복으로 곱셈을 저장합니다. 컴파일러가 다양한 수준의 공격성으로 조정하는 추가 최적화 형식은 다음과 같이 변환됩니다.

if (n > 0)
{
  int temp1 = n*scale;
  int *temp2 = foo;
  do
  {
    temp1 -= scale;
    *temp2++ = temp1;
  } while(temp1);
}

오버플로시 자동 랩 어라운드가있는 시스템에서도 n보다 작은 숫자가 있으면 오작동 할 수 있으며, 스케일로 곱하면 0이됩니다. 스케일이 메모리에서 두 번 이상 읽 히면 무한 루프로 바뀔 수도 있습니다. 예기치 않게 값을 변경했습니다 ( "scale"이 UB를 호출하지 않고 중간 루프를 변경할 수있는 경우 컴파일러는 최적화를 수행 할 수 없습니다).

대부분의 이러한 최적화는 두 개의 짧은 부호없는 유형을 곱하여 INT_MAX + 1과 UINT_MAX 사이의 값을 생성하는 경우 문제가 없지만 gcc는 루프 내에서 이러한 곱셈으로 인해 루프가 조기 종료 될 수있는 경우가 있습니다. . 생성 된 코드의 비교 명령에서 비롯된 이러한 동작은 발견하지 못했지만 컴파일러가 오버플로를 사용하여 루프가 최대 4 회 이하로 실행될 수 있다고 추론하는 경우 관찰 할 수 있습니다. 추론으로 인해 루프의 상한이 무시 되더라도 일부 입력이 UB를 유발하고 다른 입력이 UB를 유발하지 않는 경우 기본적으로 경고를 생성하지 않습니다.


4

정의되지 않은 동작은 정의상 회색 영역입니다. 당신은 단순히 그것이 무엇을 할 것인지,하지 않을 것인지를 예측할 수 없습니다. 그것이 "정의되지 않은 행동"이 의미하는 바입니다 .

옛날부터 프로그래머들은 정의되지 않은 상황에서 정의의 잔재를 구하기 위해 항상 노력해 왔습니다. 그들은 정말로 사용하고 싶은 코드를 가지고 있지만 정의되지 않은 것으로 판명되었습니다. 그래서 그들은 다음과 같이 주장하려고합니다 : "정의되지 않은 것은 알고 있지만, 최악의 경우에는이 작업 수행 할 것입니다. 절대 그렇게하지 않을 것 입니다. . " 그리고 때로는 이러한 주장이 다소 옳지 만 종종 틀립니다. 컴파일러가 점점 더 똑똑 해짐에 따라 (또는 어떤 사람들은 더 스 나이키와 스 니키라고 말할 수도 있습니다) 질문의 경계는 계속 변합니다.

따라서 실제로 작동이 보장되고 오랫동안 작동하는 코드를 작성하려면 한 가지 선택이 있습니다. 정의되지 않은 동작은 피해야합니다. 진실로, 당신이 그것에 손을 대면 그것은 당신을 괴롭히기 위해 돌아올 것입니다.


하지만 여기에 문제가 있습니다. 컴파일러는 정의되지 않은 동작을 사용하여 최적화 할 수 있지만 일반적으로 사용자에게 말하지 않습니다. 그래서 우리가 어떤 대가를 치르더라도 X를 피해야하는이 멋진 도구를 가지고 있다면 왜 컴파일러가 경고를해서 문제를 고칠 수 없을까요?
Jason S

1

귀하의 예에서 고려하지 않는 한 가지는 최적화입니다. a루프에 설정되어 있지만 사용되지 않았으며 옵티마이 저가이 문제를 해결할 수 있습니다. 따라서 옵티마이 저가 a완전히 폐기하는 것은 합법적 이며,이 경우 정의되지 않은 모든 행동은 부줌의 희생자처럼 사라집니다.

그러나 물론 이것은 최적화가 정의되지 않았기 때문에 정의되지 않았습니다. :)


1
동작이 정의되지 않았는지 여부를 결정할 때 최적화를 고려할 이유가 없습니다.
Keith Thompson

2
프로그램이 예상대로 동작한다고해서 정의되지 않은 동작이 "사라진다"는 의미는 아닙니다. 행동은 여전히 ​​정의되지 않았으며 단순히 운에 의존하고 있습니다. 프로그램의 동작이 컴파일러 옵션에 따라 변경 될 수 있다는 사실은 동작이 정의되지 않았다는 강력한 지표입니다.
Jordan Melo 2016 년

@JordanMelo 이전 답변 중 많은 부분이 최적화에 대해 논의했기 때문에 (그리고 OP가 특별히 그것에 대해 질문했습니다), 이전 답변이 다루지 않은 최적화 기능에 대해 언급했습니다. 또한 최적화 가이를 제거 할 있지만 특정 방식으로 작동하는 최적화에 대한 의존은 다시 정의되지 않는다는 점을 지적했습니다 . 나는 확실히 그것을 추천하지 않습니다! :)
그레이엄

@KeithThompson 물론이지, OP는 최적화와 그가 플랫폼에서 볼 수있는 정의되지 않은 동작에 미치는 영향에 대해 구체적으로 물었습니다. 최적화에 따라 특정 동작이 사라질 수 있습니다. 그래도 내 대답에서 말했듯이 정의되지 않음은 그렇지 않습니다.
Graham

0

이 질문은 이중 태그 C와 C ++이므로 두 가지 모두를 시도하고 해결하겠습니다. 여기서 C와 C ++는 다른 접근 방식을 취합니다.

C에서 구현은 정의되지 않은 동작이있는 것처럼 전체 프로그램을 처리하기 위해 정의되지 않은 동작이 호출된다는 것을 증명할 수 있어야합니다. OPs 예제에서 컴파일러가이를 증명하는 것은 사소한 것처럼 보이므로 전체 프로그램이 정의되지 않은 것처럼 보입니다.

결함 보고서 109에서 이를 확인할 수 있습니다 .

그러나 C 표준이 "정의되지 않은 값"(단순한 생성이 전적으로 "정의되지 않은 동작"을 포함하지 않음)의 별도의 존재를 인식하면 컴파일러 테스트를 수행하는 사람은 다음과 같은 테스트 케이스를 작성할 수 있으며 예상 할 수 있습니다. (또는 요구할 수있는) 준수 구현이 최소한이 코드를 "실패"없이 컴파일 (그리고 실행하도록 허용)해야합니다.

int array1[5];
int array2[5];
int *p1 = &array1[0];
int *p2 = &array2[0];

int foo()
{
int i;
i = (p1 > p2); /* Must this be "successfully translated"? */
1/0; /* Must this be "successfully translated"? */
return 0;
}

그래서 결론은 이것이다 : 위의 코드가 "성공적으로 번역"되어야 하는가? (5.1.1.3 항에 첨부 된 각주 참조)

응답은 다음과 같습니다.

C 표준은 "정의되지 않은 값"이 아니라 "결정되지 않은 값"이라는 용어를 사용합니다. 값이 결정되지 않은 개체를 사용하면 정의되지 않은 동작이 발생합니다. 하위 절 5.1.1.3의 각주는 유효한 프로그램이 여전히 올바르게 번역되어있는 한 구현이 임의의 수의 진단을 생성 할 수 있음을 지적합니다. 평가로 인해 정의되지 않은 동작이 발생하는식이 상수식이 필요한 컨텍스트에 나타나면 포함하는 프로그램이 엄격하게 준수되지 않습니다. 더욱이, 주어진 프로그램의 모든 가능한 실행이 정의되지 않은 동작을 초래한다면, 주어진 프로그램은 엄격하게 준수하지 않습니다. 준수 구현은 단순히 해당 프로그램의 일부 가능한 실행으로 인해 정의되지 않은 동작이 발생하기 때문에 엄격하게 준수하는 프로그램을 변환하는 데 실패해서는 안됩니다. foo는 절대 호출되지 않을 수 있으므로 주어진 예제는 준수 구현에 의해 성공적으로 번역되어야합니다.

C ++에서 접근 방식은 더 편안해 보이며 구현이 정적으로 증명할 수 있는지 여부에 관계없이 프로그램에 정의되지 않은 동작이 있음을 시사합니다.

다음과 같은 [intro.abstrac] p5 가 있습니다.

잘 구성된 프로그램을 실행하는 준수 구현은 동일한 프로그램 및 동일한 입력을 사용하여 추상 기계의 해당 인스턴스의 가능한 실행 중 하나와 동일한 관찰 가능한 동작을 생성해야합니다. 그러나 이러한 실행에 정의되지 않은 작업이 포함되어있는 경우이 문서는 해당 입력으로 해당 프로그램을 실행하는 구현에 대한 요구 사항을 지정하지 않습니다 (정의되지 않은 첫 번째 작업 이전의 작업에 대해서도).


함수를 실행하면 UB가 호출된다는 사실은 특정 입력이 주어 졌을 때 프로그램이 동작하는 방식에만 영향을 미칠 수 있습니다. 해당 입력이 주어 졌을 때 프로그램의 가능한 실행 중 하나가 UB를 호출하는 경우에만 가능합니다. 함수 호출이 UB를 호출한다는 사실은 함수 호출을 허용하지 않는 입력이 제공 될 때 프로그램이 정의 된 동작을 갖는 것을 방지하지 않습니다.
supercat

@supercat 나는 그것이 우리가 적어도 C에 대해 말하는 내 대답이라고 믿습니다.
Shafik Yaghmour

"Any such execution"이라는 문구는 특정 입력으로 프로그램을 실행할 수있는 방법을 나타 내기 때문에 C ++에서 인용 된 텍스트에도 동일하게 적용된다고 생각합니다. 특정 입력으로 인해 함수가 실행되지 않으면 인용 된 텍스트에서 그러한 함수의 모든 것이 UB가 될 것이라고 말하는 내용이 없습니다.
supercat

-2

가장 큰 대답은 잘못된 (그러나 일반적인) 오해입니다.

정의되지 않은 동작은 런타임 속성입니다 *. 그것은 "시간 여행"할 수 없습니다 !

특정 작업은 표준에 의해 정의되어 부작용이 있으며 최적화 할 수 없습니다. I / O를 수행하거나 volatile변수 에 액세스하는 작업 이이 범주에 속합니다.

그러나 주의 할 점이 있습니다. UB는 이전 작업 을 취소 하는 동작을 포함하여 모든 동작 이 될 수 있습니다 . 이는 경우에 따라 이전 코드를 최적화하는 것과 유사한 결과를 가져올 수 있습니다.

사실, 이것은 최상위 답변의 인용문과 일치합니다 (강조 표시).

잘 구성된 프로그램을 실행하는 준수 구현은 동일한 프로그램 및 동일한 입력을 사용하여 추상 기계의 해당 인스턴스의 가능한 실행 중 하나와 동일한 관찰 가능한 동작을 생성해야합니다.
그러나 이러한 실행에 정의되지 않은 작업이 포함되어있는 경우이 국제 표준 은 해당 입력으로 해당 프로그램을 실행 하는 구현에 대한 요구 사항을 지정 하지 않습니다 (첫 번째 정의되지 않은 작업 이전의 작업에 대해서도).

예,이 인용문 "정의되지 않은 첫 번째 작업 이전의 작업에 대해서도" 라고 말하지만 , 이것은 단순히 컴파일 된 것이 아니라 실행 중인 코드에 대한 것임을 주목하십시오 .
결국 실제로 도달하지 않은 정의되지 않은 동작은 아무 작업도 수행하지 않으며 UB가 포함 된 행에 실제로 도달하려면 선행 코드가 먼저 실행되어야합니다!

예, UB가 실행 되면 이전 작업의 효과가 정의되지 않습니다. 그러나 그것이 일어날 때까지 프로그램의 실행은 잘 정의되어 있습니다.

그러나 이러한 일이 발생하는 프로그램의 모든 실행은 이전 작업을 수행했지만 그 효과를 취소 하는 프로그램을 포함 하여 동등한 프로그램에 최적화 될 수 있습니다 . 결과적으로 이전 코드는 그 효과가 취소되는 것과 동일 할 때마다 최적화 될 수 있습니다 . 그렇지 않으면 할 수 없습니다. 예는 아래를 참조하십시오.

* 참고 : 이것은 컴파일 타임에 발생하는 UB 와 일치 하지 않습니다 . 컴파일러가 실제로 UB 코드 모든 입력에 대해 항상 실행 된다는 것을 증명할 수 있다면 UB는 컴파일 시간으로 확장 할 수 있습니다. 그러나이를 위해서는 이전의 모든 코드가 결국를 반환 한다는 사실을 알아야 하며 이는 강력한 요구 사항입니다. 다시 한 번 예 / 설명은 아래를 참조하십시오.


이를 구체적으로 설명하려면 다음 코드 뒤에 오는 foo정의되지 않은 동작에 관계없이 인쇄 하고 입력을 기다려야합니다.

printf("foo");
getchar();
*(char*)1 = 1;

그러나 fooUB가 발생한 후 화면에 남아 있거나 입력 한 문자가 더 이상 입력 버퍼에 없다는 보장은 없습니다 . 이 두 작업 모두 "취소"할 수 있으며 UB "시간 이동"과 유사한 효과를 갖습니다.

는 IF getchar()라인이 아니었다는 것이다 선이 멀리 최적화 할 법적 될 경우에만 그리고 만약 그 것이다 구별 출력에서 foo다음과 "을 취소하고".

둘을 구별 할 수 없는지 여부는 전적으로 구현 (예 : 컴파일러 및 표준 라이브러리)에 따라 다릅니다 . 예를 들어, 다른 프로그램이 출력을 읽을 때까지 기다리는 동안 여기에서 스레드를 printf 차단할 수 있습니까? 아니면 즉시 반환됩니까?

  • 여기에서 차단할 수 있으면 다른 프로그램이 전체 출력 읽기를 거부 할 수 있으며 반환되지 않을 수 있으며 결과적으로 UB가 실제로 발생하지 않을 수 있습니다.

  • 여기에서 즉시 반환 될 수 있다면 반환해야한다는 것을 알고 있으므로이를 최적화하는 것은 실행 한 다음 그 효과를 취소하는 것과 완전히 구별 할 수 없습니다.

물론 컴파일러는 특정 버전의 printf에서 허용되는 동작을 알고 있으므로 그에 따라 최적화 할 수 있으며 결과적으로 printf다른 경우가 아닌 경우에 최적화 될 수 있습니다. 그러나 다시 말하지만, UB로 인해 이전 코드가 "중독"된 것이 아니라 이전 작업을 취소 한 UB와 구별 할 수 없다는 것이 정당 합니다.


1
표준을 완전히 오독하고 있습니다. 프로그램을 실행할 때 동작 이 정의되지 않았다고 말합니다 . 기간. 이 대답은 100 % 틀 렸습니다. 표준은 매우 명확합니다. 순진한 실행 흐름의 어느 시점에서든 UB를 생성하는 입력으로 프로그램을 실행하는 것은 정의되지 않았습니다.
David Schwartz

@DavidSchwartz : 논리적 인 결론에 대한 해석을 따른다면 논리적 인 의미가 없다는 것을 깨달아야합니다. 입력은 프로그램이 시작될 때 완전히 결정되는 것이 아닙니다 . 주어진 줄에서 프로그램에 대한 입력 (단순한 존재 조차도 )은 해당 줄 까지 프로그램의 모든 부작용에 의존 할 수 있습니다. 따라서 프로그램 UB 라인 앞에 오는 부작용을 피할 수 없습니다. 그 이유는 환경과의 상호 작용이 필요하기 때문에 UB 라인이 처음에 도달할지 여부에 영향을 미치기 때문입니다.
user541686

3
그건 중요하지 않습니다. 정말. 다시 말하지만, 상상력이 부족합니다. 예를 들어 컴파일러가 호환 코드가 차이를 구분할 수 없다고 말할 수 있다면 UB 인 코드를 이동하여 순진하게 "선행"할 것으로 예상되는 출력보다 먼저 UB 부분이 실행되도록 할 수 있습니다.
David Schwartz

2
@Mehrdad : 아마도 UB는 행동을 정의한 현실 세계에서 어떤 일이 일어날 수 있었던 마지막 지점을지나 시간 여행을 할 수 없다고 말하는 것입니다. 구현시 입력 버퍼를 검사하여 getchar ()에 대한 다음 1000 번의 호출이 차단 될 수있는 방법이 없다고 판단 할 수 있고 1000 번째 호출 이후에 UB가 발생한다고 판단 할 수 있다면 다음 중 어떤 것도 수행 할 필요가 없습니다. 전화. 그러나 구현이 이전의 모든 출력이 다음을 가질 때까지 실행이 getchar ()를 전달하지 않도록 지정하는 경우 ...
supercat

2
... 300-baud 터미널로 전달되었고 그 전에 발생하는 모든 control-C는 그 앞에있는 버퍼에 다른 문자가 있어도 getchar ()가 신호를 발생시키는 원인이되며, 그런 구현은 getchar () 이전의 마지막 출력을 지나서 UB를 이동합니다. 까다로운 것은 어떤 경우에 컴파일러가 프로그래머를 통과 할 것으로 예상되어야하는지 아는 것입니다. 라이브러리 구현이 표준에서 요구하는 것 이상으로 제공 할 수있는 행동 보장을 제공 할 수 있습니다.
supercat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.