code

정의되지 않은 동작을 가진 분기는 도달 할 수없는 것으로 간주되고 데드 코드로 최적화 될 수 있습니까?

codestyles 2020. 9. 10. 07:54
반응형

정의되지 않은 동작을 가진 분기는 도달 할 수없는 것으로 간주되고 데드 코드로 최적화 될 수 있습니까?


다음 진술을 고려하십시오.

*((char*)NULL) = 0; //undefined behavior

정의되지 않은 동작을 명확하게 호출합니다. 주어진 프로그램에 그러한 명령문이 존재한다는 것은 전체 프로그램이 정의되지 않았거나 제어 흐름이이 명령문에 도달하면 동작이 정의되지 않음을 의미합니까?

사용자가 번호를 입력하지 않는 경우 다음 프로그램이 잘 정의되어 3있습니까?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

아니면 사용자가 무엇을 입력하든 완전히 정의되지 않은 동작입니까?

또한 컴파일러는 정의되지 않은 동작이 런타임에 실행되지 않는다고 가정 할 수 있습니까? 그것은 시간을 거꾸로 추론 할 수 있습니다.

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

여기에서 컴파일러는 num == 3우리가 항상 정의되지 않은 동작을 호출하는 경우를 추론 할 수 있습니다. 따라서이 경우는 불가능하며 번호를 인쇄 할 필요가 없습니다. 전체 if문장을 최적화 할 수 있습니다. 표준에 따라 이런 종류의 역 추론이 허용됩니까?


주어진 프로그램에 그러한 명령문이 존재한다는 것은 전체 프로그램이 정의되지 않았거나 제어 흐름이이 명령문에 도달하면 동작이 정의되지 않음을 의미합니까?

둘 다 아닙니다. 첫 번째 조건은 너무 강하고 두 번째 조건은 너무 약합니다.

개체 액세스는 때때로 순서가 지정되지만 표준은 시간 외의 프로그램 동작을 설명합니다. Danvil은 이미 인용했습니다.

그러한 실행에 정의되지 않은 작업이 포함 된 경우이 국제 표준은 해당 입력으로 해당 프로그램을 실행하는 구현에 대한 요구 사항을 지정하지 않습니다 (정의되지 않은 첫 번째 작업 이전의 작업에 대해서도).

이것은 다음과 같이 해석 될 수 있습니다.

프로그램 실행에서 정의되지 않은 동작이 발생하면 전체 프로그램에 정의되지 않은 동작이있는 것입니다.

따라서 UB로 연결할 수없는 문은 프로그램에 UB를 제공하지 않습니다. (입력 값으로 인해) 도달 할 수없는 도달 가능한 문은 프로그램에 UB를 제공하지 않습니다. 그것이 당신의 첫 번째 상태가 너무 강한 이유입니다.

이제 컴파일러는 일반적으로 UB가 무엇인지 알 수 없습니다. 따라서 옵티마이 저가 동작을 정의 할 경우 순서를 변경할 수있는 잠재적 UB를 사용하여 문을 다시 정렬 할 수 있도록하려면 UB가 "시간을 거슬러 올라가고"이전 시퀀스 지점 (또는 C ++ 11 용어, UB가 UB 사물 이전에 시퀀싱되는 사물에 영향을 미칩니다). 따라서 두 번째 상태가 너무 약합니다.

이에 대한 주요 예는 최적화 프로그램이 엄격한 앨리어싱에 의존하는 경우입니다. 엄격한 앨리어싱 규칙의 요점은 문제의 포인터가 동일한 메모리의 앨리어싱을 할 수있는 경우 컴파일러가 유효하게 다시 정렬 할 수없는 작업을 다시 정렬 할 수 있도록하는 것입니다. 따라서 불법 앨리어싱 포인터를 사용하고 UB가 발생하면 UB 문 "앞의"문에 쉽게 영향을 줄 수 있습니다. 추상 기계에 관한 한 UB 문은 아직 실행되지 않았습니다. 실제 개체 코드에 관한 한 부분적으로 또는 완전히 실행되었습니다. 그러나 표준은 옵티마이 저가 명령문을 재정렬하는 것이 무엇을 의미하는지 또는 UB에 대한 의미에 대해 자세히 설명하지 않습니다. 그것은 단지 그것이 원하는대로 잘못 될 수있는 구현 라이센스를 부여합니다.

이것을 "UB에는 타임머신이 있습니다"라고 생각할 수 있습니다.

특히 귀하의 예에 답하십시오.

  • 3을 읽는 경우에만 동작이 정의되지 않습니다.
  • 컴파일러는 기본 블록에 정의되지 않은 작업이 포함 된 경우 코드를 죽은 것으로 제거 할 수 있습니다. 기본 블록은 아니지만 모든 분기가 UB로 연결되는 경우 허용됩니다 (그리고 저는 그렇게 생각합니다). 이 예는 PrintToConsole(3)반드시 반환 할 것으로 알려진 경우 아니면 후보가 아닙니다 . 예외를 던질 수 있습니다.

두 번째와 유사한 예는 다음 -fdelete-null-pointer-checks과 같은 코드를 취할 수 있는 gcc 옵션입니다 (이 특정 예를 확인하지 않았으므로 일반적인 아이디어를 설명하는 것으로 간주).

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

다음으로 변경하십시오.

*p = 3;
std::cout << "3\n";

왜? 경우가 있기 때문에 p널 (null)이 그 다음이다를 가정 할 수 컴파일러가 따라 널 (null)와 최적화되지 않도록 코드는, 어쨌든 UB있다. 이 걸리지 리눅스 커널 ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) 기본적으로는 널 포인터가 역 참조 모드에서 작동하기 때문에 하지 않는 가정 UB이면 커널이 처리 할 수있는 정의 된 하드웨어 예외가 발생할 것으로 예상됩니다. 최적화가 활성화되면 gcc는 -fno-delete-null-pointer-checks표준 이상 보장을 제공하기 위해를 사용해야합니다 .

추신 : "정의되지 않은 행동은 언제 발생합니까?"라는 질문에 대한 실질적인 대답입니다. "하루 떠날 계획 10 분 전"입니다.


1.9 / 4의 표준 상태

[ Note: This International Standard imposes no requirements on the behavior of programs that contain undefined behavior. — end note ]

The interesting point is probably what "contain" means. A little later at 1.9/5 it states:

However, if any such execution contains an undefined operation, this International Standard places no requirement on the implementation executing that program with that input (not even with regard to operations preceding the first undefined operation)

Here it specifically mentions "execution ... with that input". I would interpret that as, undefined behaviour in one possible branch which is not executed right now does not influence the current branch of execution.

A different issue however are assumptions based on undefined behaviour during code generation. See the answer of Steve Jessop for more details about that.


An instructive example is

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

Both current GCC and current Clang will optimize this (on x86) to

xorl %eax,%eax
ret

because they deduce that x is always zero from the UB in the if (x) control path. GCC won't even give you a use-of-uninitialized-value warning! (because the pass that applies the above logic runs before the pass that generates uninitialized-value warnings)


The current C++ working draft says in 1.9.4 that

This International Standard imposes no requirements on the behavior of programs that contain undefined behavior.

Based on this, I would say that a program containing undefined behavior on any execution path can do anything at every time of its execution.

There are two really good articles on undefined behavior and what compilers usually do:


The word "behavior" means something is being done. A statemenr that is never executed is not "behavior".

An illustration:

*ptr = 0;

Is that undefined behavior? Suppose we are 100% certain ptr == nullptr at least once during program execution. The answer should be yes.

What about this?

 if (ptr) *ptr = 0;

Is that undefined? (Remember ptr == nullptr at least once?) I sure hope not, otherwise you won't be able to write any useful program at all.

No srandardese was harmed in the making of this answer.


The undefined behavior strikes when the program will cause undefined behavior no matter what happens next. However, you gave the following example.

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Unless the compiler knows definition of PrintToConsole, it cannot remove if (num == 3) conditional. Let's assume that you have LongAndCamelCaseStdio.h system header with the following declaration of PrintToConsole.

void PrintToConsole(int);

Nothing too helpful, all right. Now, let's see how evil (or perhaps not so evil, undefined behavior could have been worse) the vendor is, by checking actual definition of this function.

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

The compiler actually has to assume that any arbitrary function the compiler doesn't know what does it do may exit or throw an exception (in case of C++). You can notice that *((char*)NULL) = 0; won't be executed, as the execution won't continue after PrintToConsole call.

The undefined behavior strikes when PrintToConsole actually returns. The compiler expects this not to happen (as this would cause the program to execute undefined behavior no matter what), therefore anything can happen.

However, let's consider something else. Let's say we are doing null check, and use the variable after null check.

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

In this case, it's easy to notice that lol_null_check requires a non-NULL pointer. Assigning to the global non-volatile warning variable is not something that could exit the program or throw any exception. The pointer is also non-volatile, so it cannot magically change its value in middle of function (if it does, it's undefined behavior). Calling lol_null_check(NULL) will cause undefined behavior which may cause the variable to not be assigned (because at this point, the fact that the program executes the undefined behavior is known).

However, the undefined behavior means the program can do anything. Therefore, nothing stops the undefined behavior from going back in the time, and crashing your program before first line of int main() executes. It's undefined behavior, it doesn't have to make sense. It may as well crash after typing 3, but the undefined behavior will go back in time, and crash before you even type 3. And who knows, perhaps undefined behavior will overwrite your system RAM, and cause your system to crash 2 weeks later, while your undefined program is not running.


If the program reaches a statement that invokes undefined behavior, no requirements are placed on any of the program's output/behavior whatsoever; it doesn't matter whether they would take place "before" or "after" undefined behavior is invoked.

Your reasoning about all three code snippets is correct. In particular, a compiler may treat any statement which unconditionally invokes undefined behavior the way GCC treats __builtin_unreachable(): as an optimization hint that the statement is unreachable (and thereby, that all code paths leading unconditionally to it are also unreachable). Other similar optimizations are of course possible.


Many standards for many kinds of things expend a lot of effort on describing things which implementations SHOULD or SHOULD NOT do, using nomenclature similar to that defined in IETF RFC 2119 (though not necessarily citing the definitions in that document). In many cases, descriptions of things that implementations should do except in cases where they would be useless or impractical are more important than the requirements to which all conforming implementations must conform.

Unfortunately, C and C++ Standards tend to eschew descriptions of things which, while not 100% required, should nonetheless be expected of quality implementations which don't document contrary behavior. A suggestion that implementations should do something might be seen as implying that those which don't are inferior, and in cases where it would generally be obvious which behaviors would be useful or practical, versus impractical and useless, on a given implementation, there was little perceived need for the Standard to interfere with such judgments.

A clever compiler could conform to the Standard while eliminating any code that would have no effect except when code receives inputs that would inevitably cause Undefined Behavior, but "clever" and "dumb" are not antonyms. The fact that the authors of the Standard decided that there might be some kinds of implementations where behaving usefully in a given situation would be useless and impractical does not imply any judgment as to whether such behaviors should be considered practical and useful on others. If an implementation could uphold a behavioral guarantee for no cost beyond the loss of a "dead-branch" pruning opportunity, almost any value user code could receive from that guarantee would exceed the cost of providing it. Dead-branch elimination may be fine in cases where it wouldn't require giving up anything, but if in a given situation user code could have handled almost any possible behavior other than dead-branch elimination, any effort user code would have to expend to avoid UB would likely exceed the value achieved from DBE.

참고URL : https://stackoverflow.com/questions/23153445/can-branches-with-undefined-behavior-be-assumed-unreachable-and-optimized-as-dea

반응형