이것은 여기에 게시 할 수없는 훨씬 더 복잡한 코드를 포함하는 내 질문을 설명하는 예입니다.
#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 ++는 다르지만 두 언어가 다르면 두 언어에 대한 답변에 관심이 있기 때문에 다릅니다.)
답변
순전히 이론적 인 답변에 관심이 있다면 C ++ 표준은 정의되지 않은 동작을 “시간 여행”에 허용합니다.
[intro.execution]/5:
잘 구성된 프로그램을 실행하는 준수 구현은 동일한 프로그램 및 동일한 입력을 사용하여 추상 기계의 해당 인스턴스의 가능한 실행 중 하나와 동일한 관찰 가능한 동작을 생성해야합니다. 그러나 그러한 실행에 정의되지 않은 작업이 포함되어있는 경우이 국제 표준은 해당 입력으로 해당 프로그램을 실행하는 구현에 대한 요구 사항을 지정하지 않습니다 (첫 번째 정의되지 않은 작업 이전의 작업에 대해서도).
따라서 프로그램에 정의되지 않은 동작이 포함되어 있으면 전체 프로그램 의 동작 이 정의되지 않습니다.
답변
먼저이 질문의 제목을 수정하겠습니다.
정의되지 않은 동작은 (특히) 실행 영역이 아닙니다.
정의되지 않은 동작은 컴파일, 링크,로드 및 실행과 같은 모든 단계에 영향을줍니다.
이를 확고히하기위한 몇 가지 예는 완전한 섹션이 없다는 것을 명심하십시오.
- 컴파일러는 Undefined Behavior가 포함 된 코드 부분이 실행되지 않는다고 가정 할 수 있으므로 이러한 동작으로 연결되는 실행 경로가 데드 코드라고 가정합니다. 참조 모든 C 프로그래머가 정의되지 않은 동작에 대해 알아야 할 사항 Chris Lattner 외에는 에 .
- 링커는 약한 기호 (이름으로 인식됨)의 여러 정의가있는 경우 단일 정의 규칙 덕분에 모든 정의가 동일하다고 가정 할 수 있습니다.
- 로더 (동적 라이브러리를 사용하는 경우)는 동일하다고 가정 할 수 있으므로 찾은 첫 번째 기호를 선택합니다. 이것은 일반적으로 (ab)
LD_PRELOAD
Unix에서 트릭을 사용하여 호출을 가로채는 데 사용됩니다. - 매달린 포인터를 사용하면 실행이 실패 할 수 있습니다 (SIGSEV).
이것이 Undefined Behavior에 대해 너무나 무서운 것입니다. 정확한 동작이 발생하는지 미리 예측하는 것은 거의 불가능하며,이 예측은 도구 체인, 기본 OS 등이 업데이트 될 때마다 재검토되어야합니다.
Michael Spencer (LLVM 개발자) : CppCon 2016 : My Little Optimizer : Undefined Behavior is Magic 이 비디오를 시청하는 것이 좋습니다 .
답변
16 비트 int
를 대상으로하는 적극적으로 최적화 된 C 또는 C ++ 컴파일러 는 유형 에 추가 할 때 의 동작 이 정의되지 않음 을 알게 됩니다 .1000000000
int
이 원하는 무엇이든 할 하나의 표준으로 허용 할 수 떠나, 전체 프로그램의 삭제를 포함를 int main(){}
.
그러나 더 큰 int
s는 어떻습니까? 이 작업을 수행하는 컴파일러는 아직 모릅니다 (그리고 저는 C 및 C ++ 컴파일러 설계 전문가가 아닙니다).하지만 때때로 32 비트 int
이상을 대상으로하는 컴파일러 가 루프를 알아낼 것이라고 생각합니다. 무한 ( i
변경되지 않음) 과 너무 a
것이다 결국 오버 플로우. 따라서 다시 한 번 출력을 int main(){}
. 내가 여기서 말하고자하는 요점은 컴파일러 최적화가 점진적으로 더 공격적이됨에 따라 점점 더 정의되지 않은 동작 구조가 예기치 않은 방식으로 나타나고 있다는 것입니다.
루프가 무한하다는 사실은 루프 본문의 표준 출력에 쓰고 있기 때문에 그 자체로 정의되지 않은 것은 아닙니다.
답변
기술적으로 C ++ 표준에서 프로그램에 정의되지 않은 동작이 포함 된 경우 전체 프로그램의 동작, 컴파일 타임 (프로그램이 실행되기 전) 에서도 이 정의되지 않습니다.
실제로 컴파일러는 오버플로가 발생하지 않을 것이라고 (최적화의 일부로) 가정 할 수 있기 때문에 적어도 루프의 세 번째 반복 (32 비트 머신 가정)에서 프로그램의 동작은 정의되지 않습니다. 세 번째 반복 전에 올바른 결과를 얻을 가능성이 높습니다. 그러나 전체 프로그램의 동작이 기술적으로 정의되지 않았기 때문에 프로그램이 완전히 잘못된 출력 (출력 없음 포함)을 생성하거나 실행 중 어느 시점에서든 런타임에 충돌하거나 심지어 컴파일에 실패하는 것을 막을 수 없습니다 (정의되지 않은 동작이 다음과 같이 확장 됨). 컴파일 시간).
정의되지 않은 동작은 코드가 수행해야하는 작업에 대한 특정 가정을 제거하므로 컴파일러에 더 많은 최적화 공간을 제공합니다. 이렇게하면 정의되지 않은 동작과 관련된 가정에 의존하는 프로그램이 예상대로 작동하지 않을 수 있습니다. 따라서 C ++ 표준에 따라 정의되지 않은 것으로 간주되는 특정 동작에 의존해서는 안됩니다.
답변
@TartanLlama가 적절하게 표현한 것처럼 정의되지 않은 동작이 ‘시간 여행’이 가능한 이유 를 이해하기 위해 ‘as-if’규칙을 살펴 보겠습니다.
1.9 프로그램 실행
1 이 국제 표준의 의미 설명은 매개 변수화 된 비 결정적 추상 기계를 정의합니다. 이 국제 표준은 준수 구현의 구조에 대한 요구 사항을 지정하지 않습니다. 특히 추상 기계의 구조를 복사하거나 에뮬레이트 할 필요가 없습니다. 오히려, 아래에 설명 된 것처럼 추상 기계의 관찰 가능한 동작을 에뮬레이트하기 위해서는 준수 구현이 필요합니다.
이를 통해 프로그램을 입력과 출력이있는 ‘블랙 박스’로 볼 수 있습니다. 입력은 사용자 입력, 파일 및 기타 여러 가지가 될 수 있습니다. 출력은 표준에 언급 된 ‘관찰 가능한 동작’입니다.
표준은 입력과 출력 간의 매핑 만 정의합니다. ‘예제 블랙 박스’를 설명하여이를 수행하지만 동일한 매핑을 가진 다른 모든 블랙 박스가 똑같이 유효하다고 명시 적으로 말합니다. 이것은 블랙 박스의 내용이 무관하다는 것을 의미합니다.
이를 염두에두고 정의되지 않은 동작이 특정 순간에 발생한다고 말하는 것은 이치에 맞지 않습니다. 블랙 박스 의 샘플 구현에서 언제 어디서 발생하는지 말할 수 있지만 실제 블랙 박스는 완전히 다른 것일 수 있으므로 더 이상 어디서, 언제 발생하는지 말할 수 없습니다. 이론적으로 컴파일러는 예를 들어 가능한 모든 입력을 열거하고 결과 출력을 미리 계산하도록 결정할 수 있습니다. 그러면 컴파일 중에 정의되지 않은 동작이 발생했을 것입니다.
정의되지 않은 동작은 입력과 출력 간의 매핑이 존재하지 않는 것입니다. 프로그램은 일부 입력에 대해 정의되지 않은 동작을 가질 수 있지만 다른 입력에 대해서는 정의 된 동작을 가질 수 있습니다. 그러면 입력과 출력 간의 매핑이 단순히 불완전합니다. 출력에 대한 매핑이없는 입력이 있습니다.
질문의 프로그램에는 입력에 대해 정의되지 않은 동작이 있으므로 매핑이 비어 있습니다.
답변
int
32 비트 라고 가정하면 정의되지 않은 동작이 세 번째 반복에서 발생합니다. 예를 들어 루프가 조건부로만 도달 할 수 있거나 세 번째 반복 전에 조건부로 종료 될 수있는 경우 실제로 세 번째 반복에 도달하지 않는 한 정의되지 않은 동작이 없습니다. 그러나 정의되지 않은 동작의 경우 정의되지 않은 동작 의 호출과 관련된 “과거”의 출력을 포함 하여 프로그램의 모든 출력 이 정의되지 않습니다. 예를 들어,이 경우 출력에 3 개의 “Hello”메시지가 표시된다는 보장이 없습니다.
답변
TartanLlama의 대답이 맞습니다. 정의되지 않은 동작은 컴파일 시간 동안에도 언제든지 발생할 수 있습니다. 이것은 어리석은 것처럼 보일 수 있지만 컴파일러가 필요한 작업을 수행 할 수 있도록 허용하는 핵심 기능입니다. 컴파일러가되는 것이 항상 쉬운 것은 아닙니다. 매번 사양에 명시된대로 정확하게 수행해야합니다. 그러나 때로는 특정 행동이 발생하고 있음을 증명하는 것이 엄청나게 어려울 수 있습니다. 중지 문제를 기억한다면, 특정 입력이 공급 될 때 완료되었는지 또는 무한 루프에 들어 갔는지 증명할 수없는 소프트웨어를 개발하는 것은 다소 사소한 일입니다.
우리는 컴파일러를 비관적으로 만들고 다음 명령어가 문제와 같은 중단 문제 중 하나가 될 수 있다는 두려움에 끊임없이 컴파일 할 수 있지만 이는 합리적이지 않습니다. 대신 컴파일러에게 패스를 제공합니다. 이러한 “정의되지 않은 동작”주제에 대해 책임을지지 않습니다. 정의되지 않은 행동은 매우 미묘하게 사악한 행동들로 구성되어있어 우리가 정말로 지독하고 사악한 중지 문제와 그 밖의 것들로부터 분리하는 데 어려움을 겪습니다.
내가 게시하는 것을 좋아하는 예가 있지만 소스를 잃어 버렸다는 것을 인정하므로 의역을해야합니다. 특정 버전의 MySQL에서 가져온 것입니다. MySQL에서는 사용자가 제공 한 데이터로 채워진 순환 버퍼가 있습니다. 물론 그들은 데이터가 버퍼를 넘치지 않는지 확인하기를 원했기 때문에 확인했습니다.
if (currentPtr + numberOfNewChars > endOfBufferPtr) { doOverflowLogic(); }
충분히 정상인 것 같습니다. 그러나 numberOfNewChars가 정말 크고 오버플로되면 어떻게 될까요? 그런 다음 래핑되고보다 작은 포인터가 endOfBufferPtr
되므로 오버플로 논리가 호출되지 않습니다. 그래서 그들은 그 전에 두 번째 수표를 추가했습니다.
if (currentPtr + numberOfNewChars < currentPtr) { detectWrapAround(); }
버퍼 오버플로 오류를 처리 한 것 같습니다. 그러나이 버퍼가 특정 버전의 데비안에서 오버플로되었다는 버그가 제출되었습니다! 신중한 조사에 따르면이 버전의 데비안은 특히 최신 버전의 gcc를 사용한 첫 번째 버전이었습니다. 이 버전의 gcc에서 컴파일러는 currentPtr + numberOfNewChars가 절대로 currentPtr보다 작은 포인터가 될 수 없다는 것을 인식했습니다. 포인터에 대한 오버플로는 정의되지 않은 동작이기 때문입니다! 그것은 gcc가 전체 검사를 최적화하기에 충분했고, 당신이 그것을 검사하기 위해 코드를 작성 했음에도 불구 하고 갑자기 버퍼 오버 플로우로부터 보호받지 못했습니다 !
이것은 사양 동작이었습니다. 모든 것이 합법적이었습니다 (내가들은 바에 따르면 gcc는 다음 버전에서이 변경 사항을 롤백했습니다). 직관적 인 행동이라고 생각하는 것은 아니지만 상상력을 조금만 확장하면이 상황의 약간의 변형이 어떻게 컴파일러의 중단 문제가 될 수 있는지 쉽게 알 수 있습니다. 이 때문에 사양 작성자는이를 “정의되지 않은 동작”으로 만들고 컴파일러가 원하는 모든 작업을 수행 할 수 있다고 말했습니다.